diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..14070a5 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,14 @@ +name: CI + +on: + push: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - run: docker compose run lab pnpm install + + - run: docker compose run lab pnpm prettier --check . diff --git a/.gitignore b/.gitignore index 753c3d9..792287c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ -data_mrs/* .DS_Store +/.pnpm-store/ +/data_mrs/ /devicePEQ/ +/node_modules/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..54e8e7d --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +/pnpm-lock.yaml diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..c4b4336 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "printWidth": 100, + "trailingComma": "all" +} diff --git a/Configuring.md b/Configuring.md index 7cf6409..aa163fa 100644 --- a/Configuring.md +++ b/Configuring.md @@ -9,12 +9,12 @@ Use Github or the email in my profile. There are two other steps to show people FR graphs that I may not be able to help with: -* Creating frequency response graphs in the first place. I have never +- Creating frequency response graphs in the first place. I have never done this and don't know all the details. You might try reading [this thread](https://www.head-fi.org/threads/general-iem-measurements-discussions.903455/) for some information to get you started, and Crin is generally happy to answer questions about measurement on [his Discord server](https://discord.gg/CtTqcCb). -* Hosting the pages. Believe it or not, I'm not a web developer and I +- Hosting the pages. Believe it or not, I'm not a web developer and I don't know that much about setting up websites. One method that may work for your purposes is to use Github Pages, which allows you to serve the contents of any Github repository as a website. The main @@ -30,12 +30,12 @@ able to help with: These are the things you definitely need to change to make sure your page works and isn't claiming it's crinacle.com. -* Set `DIR` in `config.js` and place your graphs and `phone_book.json` +- Set `DIR` in `config.js` and place your graphs and `phone_book.json` there. -* Remove or change the watermark. -* Remove the `targets`, replace them with your own, or get permission +- Remove or change the watermark. +- Remove the `targets`, replace them with your own, or get permission from Crinacle to use the ones in the CrinGraph repository. -* If you are using a free/premium model, change `premium_html` in +- If you are using a free/premium model, change `premium_html` in `config_free.js` to point to your own site(s). ## Configuring CrinGraph @@ -43,7 +43,7 @@ page works and isn't claiming it's crinacle.com. The main page used to display graphs is [graph.html](graph.html), which defines the basic structure of a page and then includes a bunch of Javascript files that do the real work (at the end of the file). -[graph_free.html](graph_free.html) is identical but uses a different +[graph_free.html](graph_free.html) is identical but uses a different configuration file to remove some functionality. You will only need to use it if you intend to have both a free and a paid graph tool. @@ -54,29 +54,29 @@ can't! Here are the current configuration parameters: -* `DIR` is the location of your FR graphs and the index for them +- `DIR` is the location of your FR graphs and the index for them (see the next section). If you are displaying a cloned repository using Github Pages, using a directory other than `data/` will make merging changes from the main CrinGraph repository easier. -* `tsvParse` is a function which takes the text of an FR file and +- `tsvParse` is a function which takes the text of an FR file and converts it to the format used internally by CrinGraph. See the next section. -* `watermark` is a function that is applied once to the graph window on +- `watermark` is a function that is applied once to the graph window on startup. The argument `svg` is a [d3](https://d3js.org/) selection and you can use any d3 functionality to draw things in it. This part must be changed, or you will end up impersonating crinacle.com! To use no watermark, just delete the whole function body. You can also delete, move, or change the image and text separately. -* `max_channel_imbalance` controls how sensitive the channel imbalance +- `max_channel_imbalance` controls how sensitive the channel imbalance detector (that red exclamation mark that can show up in a headphone's key) is. You probably don't need to change this. -* `targets` lists the available target frequency responses. If you don't +- `targets` lists the available target frequency responses. If you don't want to display any targets set it to `false`. If you do use targets, each one should be a file named `... Target.txt` in the `DIR` directory you specified. The targets which are already there were provided by Crinacle so make sure you have his permission before using them. -* `scale_smoothing` (default 1) adjusts the level of smoothing applied +- `scale_smoothing` (default 1) adjusts the level of smoothing applied at a given "Smooth:" setting. The setting will always start at 5, but its value is multiplied by `scale_smoothing` to get the actual level of smoothing. @@ -86,12 +86,12 @@ of the graph tool. They are only present in [config_free.js](config_free.js). If you don't set them the tool will be unrestricted. -* `max_compare` is the maximum number of graphs allowed at a time. -* `disallow_target` prevents target FRs from ever being loaded. -* `allow_targets` is a list of target names, and overrides +- `max_compare` is the maximum number of graphs allowed at a time. +- `disallow_target` prevents target FRs from ever being loaded. +- `allow_targets` is a list of target names, and overrides `disallow_target` for those targets, so they can be loaded. If `disallow_target` isn't set, it has no effect. -* `premium_html` is the message shown when a user tries to do something +- `premium_html` is the message shown when a user tries to do something which isn't allowed according to the previous two settings. Given that it points to Crinacle's patreon and not yours, you probably want to change it. @@ -101,11 +101,11 @@ and different channel configurations than L/R. For example, `config_hp.js` is intended for headphones and shows only the right channel with five samples per channel. -* `default_channels` is a list of channels in each measurement: it +- `default_channels` is a list of channels in each measurement: it defaults to `["L","R"]`. It's called "default" because I may add a mechanism to change it for a single sample from `phone_book.json`, but no such mechanism exists right now. -* `num_samples`, if set, is the number of samples in each channel. +- `num_samples`, if set, is the number of samples in each channel. Samples are always numbered 1 to `num_samples`. The following parameters are for setting the initial samples to display, @@ -114,9 +114,9 @@ to reflect which samples are on the graph. Copying and opening that URL will open the page with those samples shown. For these parameters a headphone or target is identified by its filename. -* `init_phones` is a list of filenames to open by default. -* `share_url` enables URL sharing. -* `page_title` sets the page title display if URL sharing is enabled. +- `init_phones` is a list of filenames to open by default. +- `share_url` enables URL sharing. +- `page_title` sets the page title display if URL sharing is enabled. ## Storing your FR files @@ -134,14 +134,11 @@ specify a different filepath. The file's contents are a list of brands, where each brand is a list of models. A simple example of a brand: ```json - { - "name": "Elysian", - "suffix": "Acoustic Labs", - "phones": [ "Artemis" - , "Eros" - , "Minerva" - , "Terminator" ] - } +{ + "name": "Elysian", + "suffix": "Acoustic Labs", + "phones": ["Artemis", "Eros", "Minerva", "Terminator"] +} ``` The only required attributes for a brand are its name ("name") and a @@ -154,25 +151,25 @@ Each item in the "phones" array corresponds to a single headphone model. While an item might just be the model name as shown above, there are other possibilities as well. Two examples should cover most use cases: -* To use a different display name ("name") and filename ("file"): `{"name":"Carbo Tenore ZH-DX200-CT","file":"Tenore"}` -* To use show multiple variants of a single model: `{"name":"Gemini","file":["Gemini","Gemini Bass"]}` +- To use a different display name ("name") and filename ("file"): `{"name":"Carbo Tenore ZH-DX200-CT","file":"Tenore"}` +- To use show multiple variants of a single model: `{"name":"Gemini","file":["Gemini","Gemini Bass"]}` The full list of options is as follows: -* "name" is the displayed model name. -* "collab" gives the name of a collaborator. If that collaborator is on +- "name" is the displayed model name. +- "collab" gives the name of a collaborator. If that collaborator is on the list of brands, the headphone will be categorized under both the main vendor and the collaborator. -* "file" gives either a single filename or a list. If a list is given, +- "file" gives either a single filename or a list. If a list is given, then "name" is used only to for the headphone's name in the selection menu. The key will use filenames for display unless one of the following options is specified. -* "suffix" is a list with the same length as the list of files. The - display name is the model name plus the variant's suffix. So , {"name":"R3","file":["R3","R3 C"],"suffix":["","Custom"]} ] +- "suffix" is a list with the same length as the list of files. The + display name is the model name plus the variant's suffix. So , {"name":"R3","file":["R3","R3 C"],"suffix":["","Custom"]} ] `{"name":"R3","file":["R3","R3 C"],"suffix":["","Custom"]}` uses files based on the names `R3` and `R3 C` but shows the names "R3" and "R3 Custom". -* "prefix" is some string that should appear at the start of each +- "prefix" is some string that should appear at the start of each filename. The display name is then the filename, except that if the prefix appears at the start of the filename it is replaced with the display name. So diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a8fc3ca --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM node:23.11-alpine3.21 + +RUN npm install --global pnpm@^10.10.0 + +WORKDIR /lab diff --git a/Documentation.md b/Documentation.md index 6b9b3c3..579a3ed 100644 --- a/Documentation.md +++ b/Documentation.md @@ -24,9 +24,10 @@ Most parts of the interface are arranged using flexboxes, and rearranged with CSS media queries to detect screen width and aspect ratio. There are three main layouts: -* The desktop layout places the graph window at the top with the selector and manager side by side below it. -* The mobile layout (for narrow screens) stacks everything vertically with the selector above the manager. -* When the screen is very wide relative to its height, the selector and manager are stacked as in the mobile layout but placed right of the graph window. + +- The desktop layout places the graph window at the top with the selector and manager side by side below it. +- The mobile layout (for narrow screens) stacks everything vertically with the selector above the manager. +- When the screen is very wide relative to its height, the selector and manager are stacked as in the mobile layout but placed right of the graph window. If the screen is narrow enough, the toolbar below the graph window will collapse to avoid clutter. The entire toolbar can be shown by clicking @@ -120,6 +121,7 @@ natural spline tends to emphasize little bumps in the data, making it worse even than linear interpolation. Mathematically, a smoothing spline minimizes a weighted sum of: + 1. All the square differences between the original and smoothed values, and 2. The integral of the square of the second derivative of the smoothed function. @@ -209,7 +211,7 @@ indicate how they might correspond. CrinGraph weights graphs using the [ISO 226:2003](https://en.wikipedia.org/wiki/Equal-loudness_contour) loudness standard (with linear rather than cubic interpolation, since it has little effect on the average) with -[free field](https://en.wikipedia.org/wiki/Free_field_(acoustics)) +[free field]() compensation (which most closely matches the conditions in which that standard was measured) to convert from speakers to IEMs. The flat bass response of the free field compensation is set to -7 dB to produce a @@ -303,8 +305,8 @@ be skipped until a better one is found, giving up after three tries. [Martin Ankerl](https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/) describes a method which selects hues with spacing based on the golden -ratio. Spacing in this way gives a one-dimensional *low-discrepancy -sequence*: a sequence in which it takes a while for values similar to +ratio. Spacing in this way gives a one-dimensional _low-discrepancy +sequence_: a sequence in which it takes a while for values similar to previous ones to appear. CrinGraph extends this technique to the three dimensions of the [HCL color space](https://en.wikipedia.org/wiki/HCL_color_space)—hue, @@ -324,9 +326,10 @@ cross-section along the ring, like a thick washer. Three modifications are made to this ring in order to account for human perception, or maybe imperfect perceptual uniformity of HCL space, or even unsuitability for lines rather than color fields. -* Hues are shifted so cool colors like blues and greens appear less often, and reds and yellows more often. -* Hues are shifted towards six colors with evenly spaced hues—the primary and secondary colors red, yellow, green, cyan, blue, and purple. -* Chroma and luminance are shifted so that yellows are brighter and bolder, and blues darker. + +- Hues are shifted so cool colors like blues and greens appear less often, and reds and yellows more often. +- Hues are shifted towards six colors with evenly spaced hues—the primary and secondary colors red, yellow, green, cyan, blue, and purple. +- Chroma and luminance are shifted so that yellows are brighter and bolder, and blues darker. Channels are separated from one another primarily by adjusting hue and chroma. Channels with different luminance don't look related. The diff --git a/README.md b/README.md index b2a7855..7566591 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,54 @@ -# The In-Ear Graphing Library +# Squiglink Lab -If you're not weirdly obsessed with headphones you can leave at any time. - -Crinacle is a reviewer famous around the world (at least, I'm on the -opposite side of it as he is) for his extensive reviews and measurements -of in-ear monitors (IEMs). CrinGraph is the tool which allows readers to -compare measurements against each other, and save easily readable images -to share around the internet. Although it was designed for -[Crin's site](https://crinacle.com/graphs/graphtool/), -the code here can be used freely by anyone, with no restrictions. -There are now many instances, including -[Banbeucmas](https://banbeu.com/graph/tool/), -[HypetheSonics](https://www.hypethesonics.com/iemdbc/), -[Rohsa](https://rohsa.gitlab.io/graphtool/), and -[Super\* Review](https://squig.link/), which has links to even more of -them. If you're interested in using it for your own graphs, see -[Configuring.md](Configuring.md) and ask me about any questions that -come up. - -### What are the squiggles? - -If you want the whole story, there's no choice but to get it from -[the man himself](https://crinacle.com/2020/04/08/graphs-101-how-to-read-headphone-measurements/). -5,000 words and you'll still be disappointed when it's over. - -The most informative headphone measurement, and the only one handled by -this tool, is the frequency response (FR) graph. An FR graph shows how -loud the headphone will render sounds at different pitches. The higher -the left portion of the graph, the more your brain will rattle; the -higher the right portion of the graph, the more your ears will bleed. -The current industry standard is a "V-shaped" response which applies -both conditions at once. Using an FR graph you may easily see which -headphones conform to this standard and which are insufficiently "fun". - -### Sample graphs - -This repository includes some sample data so that the tool can be shown -through Github pages. Sometimes I use this to show people features -before they're adopted on Crin's site. - -[View some sample graphs.](https://mlochbaum.github.io/CrinGraph/graph.html) - -Because Crinacle's frequency response measurements are not public, the -sample response curves shown are synthesized. They are not real -headphones and you can't listen to them. To reduce potential -disappointment, steps have been taken to ensure that the curves are as -uninviting as possible. Any resemblance to the exports of a large East -Asian county is purely coincidental. +The primary headphone measurement supported by this tool is the frequency response (FR) graph, which is generally considered one of the most informative metrics for evaluating headphones. An FR graph illustrates how headphones reproduce sound across a range of frequencies. In general terms, a higher level on the left side of the graph suggests more pronounced bass, while a higher level on the right side typically indicates greater treble. ## Features -If you want one that's not here, just ask so I can explain why it's a -bad idea. - -### Layout - -The graph tool displays: -* A **graph window** at the top -* The **toolbar** just below it -* The **selector** at the bottom left, or below the toolbar for narrow windows -* A **target selector** -* The **manager** for active curves - ### Graph window -* Standard logarithmic frequency (Hz) and sound pressure level (dB) [axes](Documentation.md#axes) -* [Colors](Documentation.md#colors) are persistent and algorithmically generated to ensure contrast -* Use the slider at the left to rescale and adjust the y axis -* [Hover](Documentation.md#highlight-on-mouseover) over or click a curve to see its name and highlight it in the manager +- Standard logarithmic frequency (Hz) and sound pressure level (dB) [axes](Documentation.md#axes). +- [Colors](Documentation.md#colors) are persistent and algorithmically generated to ensure contrast. +- A slider at the left to rescale and adjust the y-axis. +- [Hover](Documentation.md#highlight-on-mouseover) over or click on a curve to see its name and highlight it. ### Toolbar -* Zoom into bass, mid, or treble frequencies -* [Normalize](Documentation.md#normalization) with a target loudness or a normalization frequency -* [Smooth](Documentation.md#smoothing) graphs with a configurable parameter -* Toggle inspect mode to see the numeric response values when you mouse over the graph -* [Label](Documentation.md#labelling) curves inside the graph window -* Save a png [screenshot](Documentation.md#screenshot) of the graph (with labels) -* Recolor the active curves in case there is a color conflict -* Toolbar collapses and expands, along with the target selector, when the screen is small - -### Headphone and target selectors - -* Headphones are grouped by brand: select brands to narrow them down -* Click to select one headphone or brand and unselect others; middle or ctrl-click for non-exclusive select -* [Search](Documentation.md#searching) all brands or headphones by name -* Targets are selected the same way but are independent from headphones - -### Headphone manager - -* Curve names and colors are displayed here -* Choose and compare variant measurements of the same model with a dropdown -* Use the wishbone-shaped selector to see left and/or right channels or [average](Documentation.md#averaging) them together -* A red exclamation mark indicates that channels are [imbalanced](Documentation.md#channel-imbalance-marker) -* Change the offset to move graphs up and down (after [normalization](Documentation.md#normalization)) -* Select [BASELINE](Documentation.md#baseline) to adjust all curves so the chosen one is flat -* Temporarily hide or unhide a graph -* PIN a headphone to avoid losing it while adding others -* Click the little dots at the bottom left to change a single headphone's [color](Documentation.md#colors) - -## Contact - -File a Github issue here for topics related to the project. You can also -reach me by the email in my Github profile and the [LICENSE](LICENSE). -I can sometimes be found on -[Crin's Discord server](https://discord.gg/CtTqcCb) where I am -creatively named Marshall. +- Zoom in on bass, mid, or treble frequencies. +- [Normalize](Documentation.md#normalization) with a target loudness or a normalization frequency. +- [Smooth](Documentation.md#smoothing) graphs with a configurable parameter. +- Enable inspect mode to view numeric values when hovering over the graph. +- [Label](Documentation.md#labelling) curves directly within the graph window. +- Save a PNG [screenshot](Documentation.md#screenshot) of the graph (with labels). +- Recolor active curves to avoid color conflicts. +- The toolbar and target selector will collapse or expand based on screen size. + +### Model and target selectors + +- Models are organized by brand, select a brand to narrow the options. +- Click to select a specific model or brand, and unselect others, use MMB or Ctrl + LMB for multi-selection. +- [Search](Documentation.md#searching) all brands or models by name. +- Targets are selected in the same way but are separate from the models. + +### Model manager + +- Curve names and colors are displayed here. +- Select and compare different variants of the same model using the dropdown. +- Use the wishbone-shaped selector to view left and/or right channels, or [average](Documentation.md#averaging) them together. +- A red exclamation mark indicates that channels are [imbalanced](Documentation.md#channel-imbalance-marker). +- Adjust the offset to move graphs up or down (after [normalization](Documentation.md#normalization)). +- Select [BASELINE](Documentation.md#baseline) to flatten all curves to the chosen one. +- Temporarily hide or unhide a graph. +- Pin a model to avoid losing it while adding others. +- Click on the small dots in the bottom left to change a model's color. + +## Tips + +```sh +docker compose run lab pnpm prettier --check . +docker compose run lab pnpm prettier --write . +``` + +## Contributing + +1. Install dependencies (`docker compose run lab pnpm install`). +2. Ensure that formatters are passing. diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..f883038 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,5 @@ +services: + lab: + build: . + volumes: + - .:/lab diff --git a/config.js b/config.js index 1c1cfcc..b0cb021 100644 --- a/config.js +++ b/config.js @@ -1,303 +1,306 @@ // Configuration options -const init_phones = ["BKF"], // Optional. Which graphs to display on initial load. Note: Share URLs will override this set - DIR = "data/", // Directory where graph files are stored - default_channels = ["L","R"], // Which channels to display. Avoid javascript errors if loading just one channel per phone - default_normalization = "dB", // Sets default graph normalization mode. Accepts "dB" or "Hz" - default_norm_db = 60, // Sets default dB normalization point - default_norm_hz = 500, // Sets default Hz normalization point (500Hz is recommended by IEC) - max_channel_imbalance = 5, // Channel imbalance threshold to show ! in the channel selector - alt_layout = true, // Toggle between classic and alt layouts - alt_sticky_graph = true, // If active graphs overflows the viewport, does the graph scroll with the page or stick to the viewport? - alt_animated = false, // Determines if new graphs are drawn with a 1-second animation, or appear instantly - alt_header = true, // Display a configurable header at the top of the alt layout - alt_header_new_tab = false, // Clicking alt_header links opens in new tab - alt_tutorial = true, // Display a configurable frequency response guide below the graph - alt_augment = false, // Display augment card in phone list, e.g. review sore, shop link - site_url = 'graph.html', // URL of your graph "homepage" - share_url = true, // If true, enables shareable URLs - watermark_text = "CrinGraph", // Optional. Watermark appears behind graphs - watermark_image_url = "cringraph-logo.svg", // Optional. If image file is in same directory as config, can be just the filename - rig_description = "clone IEC 711", // Optional. Labels the graph with a description of the rig used to make the measurement, e.g. "clone IEC 711" - page_title = "CrinGraph", // Optional. Appended to the page title if share URLs are enabled - page_description = "View and compare frequency response graphs for earphones", - accessories = false, // If true, displays specified HTML at the bottom of the page. Configure further below - externalLinksBar = true, // If true, displays row of pill-shaped links at the bottom of the page. Configure further below - restricted = false, // Enables restricted mode. More restricted options below - expandable = false, // Enables button to expand iframe over the top of the parent page - expandableOnly = false, // Prevents iframe interactions unless the user has expanded it. Accepts "true" or "false" OR a pixel value; if pixel value, that is used as the maximum width at which expandableOnly is used - headerHeight = '0px', // Optional. If expandable=true, determines how much space to leave for the parent page header - themingEnabled = true, // Enable user-toggleable themes (dark mode, contrast mode) - targetDashed = false, // If true, makes target curves dashed lines - targetColorCustom = false, // If false, targets appear as a random gray value. Can replace with a fixed color value to make all targets the specified color, e.g. "black" - targetRestoreLastUsed = false, // Restore user's last-used target settings on load - labelsPosition = "default", // Up to four labels will be grouped in a specified corner. Accepts "top-left," bottom-left," "bottom-right," and "default" - stickyLabels = true, // "Sticky" labels - analyticsEnabled = true, // Enables Google Analytics 4 measurement of site usage - exportableGraphs = true, // Enables export graph button - extraEnabled = true, // Enable extra features - extraUploadEnabled = true, // Enable upload function - extraEQEnabled = true, // Enable parametic eq function - extraEQBands = 10, // Default EQ bands available - extraEQBandsMax = 20, // Max EQ bands available - extraToneGeneratorEnabled = true; // Enable tone generator function +const init_phones = ["BKF"], // Optional. Which graphs to display on initial load. Note: Share URLs will override this set + DIR = "data/", // Directory where graph files are stored + default_channels = ["L", "R"], // Which channels to display. Avoid javascript errors if loading just one channel per phone + default_normalization = "dB", // Sets default graph normalization mode. Accepts "dB" or "Hz" + default_norm_db = 60, // Sets default dB normalization point + default_norm_hz = 500, // Sets default Hz normalization point (500Hz is recommended by IEC) + max_channel_imbalance = 5, // Channel imbalance threshold to show ! in the channel selector + alt_layout = true, // Toggle between classic and alt layouts + alt_sticky_graph = true, // If active graphs overflows the viewport, does the graph scroll with the page or stick to the viewport? + alt_animated = false, // Determines if new graphs are drawn with a 1-second animation, or appear instantly + alt_header = true, // Display a configurable header at the top of the alt layout + alt_header_new_tab = false, // Clicking alt_header links opens in new tab + alt_tutorial = true, // Display a configurable frequency response guide below the graph + alt_augment = false, // Display augment card in phone list, e.g. review sore, shop link + site_url = "graph.html", // URL of your graph "homepage" + share_url = true, // If true, enables shareable URLs + watermark_text = "CrinGraph", // Optional. Watermark appears behind graphs + watermark_image_url = "cringraph-logo.svg", // Optional. If image file is in same directory as config, can be just the filename + rig_description = "clone IEC 711", // Optional. Labels the graph with a description of the rig used to make the measurement, e.g. "clone IEC 711" + page_title = "CrinGraph", // Optional. Appended to the page title if share URLs are enabled + page_description = "View and compare frequency response graphs for earphones", + accessories = false, // If true, displays specified HTML at the bottom of the page. Configure further below + externalLinksBar = true, // If true, displays row of pill-shaped links at the bottom of the page. Configure further below + restricted = false, // Enables restricted mode. More restricted options below + expandable = false, // Enables button to expand iframe over the top of the parent page + expandableOnly = false, // Prevents iframe interactions unless the user has expanded it. Accepts "true" or "false" OR a pixel value; if pixel value, that is used as the maximum width at which expandableOnly is used + headerHeight = "0px", // Optional. If expandable=true, determines how much space to leave for the parent page header + themingEnabled = true, // Enable user-toggleable themes (dark mode, contrast mode) + targetDashed = false, // If true, makes target curves dashed lines + targetColorCustom = false, // If false, targets appear as a random gray value. Can replace with a fixed color value to make all targets the specified color, e.g. "black" + targetRestoreLastUsed = false, // Restore user's last-used target settings on load + labelsPosition = "default", // Up to four labels will be grouped in a specified corner. Accepts "top-left," bottom-left," "bottom-right," and "default" + stickyLabels = true, // "Sticky" labels + analyticsEnabled = true, // Enables Google Analytics 4 measurement of site usage + exportableGraphs = true, // Enables export graph button + extraEnabled = true, // Enable extra features + extraUploadEnabled = true, // Enable upload function + extraEQEnabled = true, // Enable parametic eq function + extraEQBands = 10, // Default EQ bands available + extraEQBandsMax = 20, // Max EQ bands available + extraToneGeneratorEnabled = true; // Enable tone generator function // Specify which targets to display const targets = [ - { type:"Neutral", files:["Diffuse Field","Etymotic","Free Field","Innerfidelity ID"] }, - { type:"Reviewer", files:["Antdroid","Bad Guy","Banbeucmas","Crinacle","Precogvision","Super Review"] }, - { type:"Preference", files:["Harman","Rtings","Sonarworks"] } + { type: "Neutral", files: ["Diffuse Field", "Etymotic", "Free Field", "Innerfidelity ID"] }, + { + type: "Reviewer", + files: ["Antdroid", "Bad Guy", "Banbeucmas", "Crinacle", "Precogvision", "Super Review"], + }, + { type: "Preference", files: ["Harman", "Rtings", "Sonarworks"] }, ]; - - // ************************************************************* // Functions to support config options set above; probably don't need to change these // ************************************************************* // Set up the watermark, based on config options above function watermark(svg) { - let wm = svg.append("g") - .attr("transform", "translate("+(pad.l+W/2)+","+(pad.t+H/2-20)+")") - .attr("opacity",0.2); - - if ( watermark_image_url ) { - wm.append("image") - .attrs({x:-64, y:-64, width:128, height:128, "xlink:href":watermark_image_url}); - } - - if ( watermark_text ) { - wm.append("text") - .attrs({x:0, y:70, "font-size":28, "text-anchor":"middle", "class":"graph-name"}) - .text(watermark_text); - } - - if ( rig_description ) { - wm.append("text") - .attrs({x:380, y:-134, "font-size":8, "text-anchor":"end", "class":"rig-description"}) - .text("Measured on: " + rig_description); - } + let wm = svg + .append("g") + .attr("transform", "translate(" + (pad.l + W / 2) + "," + (pad.t + H / 2 - 20) + ")") + .attr("opacity", 0.2); + + if (watermark_image_url) { + wm.append("image").attrs({ + x: -64, + y: -64, + width: 128, + height: 128, + "xlink:href": watermark_image_url, + }); + } + + if (watermark_text) { + wm.append("text") + .attrs({ x: 0, y: 70, "font-size": 28, "text-anchor": "middle", class: "graph-name" }) + .text(watermark_text); + } + + if (rig_description) { + wm.append("text") + .attrs({ x: 380, y: -134, "font-size": 8, "text-anchor": "end", class: "rig-description" }) + .text("Measured on: " + rig_description); + } } - - // Parse fr text data from REW or AudioTool format with whatever separator function tsvParse(fr) { - return fr.split(/[\r\n]/) - .map(l => l.trim()).filter(l => l && l[0] !== '*') - .map(l => l.split(/[\s,]+/).map(e => parseFloat(e)).slice(0, 2)) - .filter(t => !isNaN(t[0]) && !isNaN(t[1])); + return fr + .split(/[\r\n]/) + .map((l) => l.trim()) + .filter((l) => l && l[0] !== "*") + .map((l) => + l + .split(/[\s,]+/) + .map((e) => parseFloat(e)) + .slice(0, 2), + ) + .filter((t) => !isNaN(t[0]) && !isNaN(t[1])); } // Apply stylesheet based layout options above function setLayout() { - function applyStylesheet(styleSheet) { - var docHead = document.querySelector("head"), - linkTag = document.createElement("link"); - - linkTag.setAttribute("rel", "stylesheet"); - linkTag.setAttribute("type", "text/css"); - - linkTag.setAttribute("href", styleSheet); - docHead.append(linkTag); - } - - if ( !alt_layout ) { - applyStylesheet("style.css"); - } else { - applyStylesheet("style-alt.css"); - applyStylesheet("style-alt-theme.css"); - } + function applyStylesheet(styleSheet) { + var docHead = document.querySelector("head"), + linkTag = document.createElement("link"); + + linkTag.setAttribute("rel", "stylesheet"); + linkTag.setAttribute("type", "text/css"); + + linkTag.setAttribute("href", styleSheet); + docHead.append(linkTag); + } + + if (!alt_layout) { + applyStylesheet("style.css"); + } else { + applyStylesheet("style-alt.css"); + applyStylesheet("style-alt-theme.css"); + } } setLayout(); - - // Set restricted mode function setRestricted() { - if ( restricted ) { - max_compare = 2; - restrict_target = false; - disallow_target = true; - premium_html = "

You gonna pay for that?

To use target curves, or more than two graphs, subscribe or upgrade to Patreon Silver tier and switch to the premium tool.

"; - } + if (restricted) { + max_compare = 2; + restrict_target = false; + disallow_target = true; + premium_html = + "

You gonna pay for that?

To use target curves, or more than two graphs, subscribe or upgrade to Patreon Silver tier and switch to the premium tool.

"; + } } setRestricted(); - - // Configure HTML accessories to appear at the bottom of the page. Displayed only if accessories (above) is true // There are a few templates here for ease of use / examples, but these variables accept any HTML -const - // Short text, center-aligned, useful for a little side info, credits, links to measurement setup, etc. - simpleAbout = ` -

This web software is based on the CrinGraph open source software project.

- `, - // Slightly different presentation to make more readable paragraphs. Useful for elaborated methodology, etc. - paragraphs = ` -

Viverra tellus in hac

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Quisque non tellus orci ac. Dictumst quisque sagittis purus sit amet volutpat consequat. Vitae nunc sed velit dignissim sodales ut. Faucibus ornare suspendisse sed nisi lacus sed viverra tellus in. Dignissim enim sit amet venenatis urna cursus eget nunc. Mi proin sed libero enim. Ut sem viverra aliquet eget sit amet. Integer enim neque volutpat ac tincidunt vitae. Tincidunt nunc pulvinar sapien et ligula ullamcorper malesuada. Mauris rhoncus aenean vel elit scelerisque mauris pellentesque. Lacus luctus accumsan tortor posuere ac ut consequat semper. Non pulvinar neque laoreet suspendisse interdum consectetur libero id faucibus. Aliquam sem et tortor consequat id. Cursus sit amet dictum sit amet justo donec. Donec adipiscing tristique risus nec feugiat in fermentum posuere.

+// Short text, center-aligned, useful for a little side info, credits, links to measurement setup, etc. +const simpleAbout = ` +

This web software is based on the CrinGraph open source software project.

+`; +// Slightly different presentation to make more readable paragraphs. Useful for elaborated methodology, etc. +const paragraphs = ` +

Viverra tellus in hac

-

Diam donec adipiscing tristique risus nec. Amet nisl purus in mollis. Et malesuada fames ac turpis egestas maecenas pharetra. Ante metus dictum at tempor commodo ullamcorper a. Dui id ornare arcu odio ut sem nulla. Ut pharetra sit amet aliquam id diam maecenas. Scelerisque in dictum non consectetur a erat nam at. In ante metus dictum at tempor. Eget nulla facilisi etiam dignissim diam quis enim lobortis scelerisque. Euismod nisi porta lorem mollis aliquam ut porttitor leo a. Malesuada proin libero nunc consequat interdum. Turpis egestas sed tempus urna et pharetra pharetra massa massa. Quis blandit turpis cursus in hac habitasse. Amet commodo nulla facilisi nullam vehicula ipsum a.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Quisque non tellus orci ac. Dictumst quisque sagittis purus sit amet volutpat consequat. Vitae nunc sed velit dignissim sodales ut. Faucibus ornare suspendisse sed nisi lacus sed viverra tellus in. Dignissim enim sit amet venenatis urna cursus eget nunc. Mi proin sed libero enim. Ut sem viverra aliquet eget sit amet. Integer enim neque volutpat ac tincidunt vitae. Tincidunt nunc pulvinar sapien et ligula ullamcorper malesuada. Mauris rhoncus aenean vel elit scelerisque mauris pellentesque. Lacus luctus accumsan tortor posuere ac ut consequat semper. Non pulvinar neque laoreet suspendisse interdum consectetur libero id faucibus. Aliquam sem et tortor consequat id. Cursus sit amet dictum sit amet justo donec. Donec adipiscing tristique risus nec feugiat in fermentum posuere.

-

Mauris ultrices eros in cursus turpis massa tincidunt. Aliquam ut porttitor leo a diam sollicitudin. Curabitur vitae nunc sed velit. Cursus metus aliquam eleifend mi in nulla posuere sollicitudin. Lectus nulla at volutpat diam ut. Nibh nisl condimentum id venenatis a condimentum vitae sapien. Tincidunt id aliquet risus feugiat in ante metus. Elementum nibh tellus molestie nunc non blandit massa enim. Ac tortor vitae purus faucibus ornare suspendisse. Pellentesque sit amet porttitor eget. Commodo quis imperdiet massa tincidunt. Nunc sed id semper risus in hendrerit gravida. Proin nibh nisl condimentum id venenatis a condimentum. Tortor at risus viverra adipiscing at in. Pharetra massa massa ultricies mi quis hendrerit dolor. Tempor id eu nisl nunc mi ipsum faucibus vitae.

+

Diam donec adipiscing tristique risus nec. Amet nisl purus in mollis. Et malesuada fames ac turpis egestas maecenas pharetra. Ante metus dictum at tempor commodo ullamcorper a. Dui id ornare arcu odio ut sem nulla. Ut pharetra sit amet aliquam id diam maecenas. Scelerisque in dictum non consectetur a erat nam at. In ante metus dictum at tempor. Eget nulla facilisi etiam dignissim diam quis enim lobortis scelerisque. Euismod nisi porta lorem mollis aliquam ut porttitor leo a. Malesuada proin libero nunc consequat interdum. Turpis egestas sed tempus urna et pharetra pharetra massa massa. Quis blandit turpis cursus in hac habitasse. Amet commodo nulla facilisi nullam vehicula ipsum a.

-

Tellus orci

+

Mauris ultrices eros in cursus turpis massa tincidunt. Aliquam ut porttitor leo a diam sollicitudin. Curabitur vitae nunc sed velit. Cursus metus aliquam eleifend mi in nulla posuere sollicitudin. Lectus nulla at volutpat diam ut. Nibh nisl condimentum id venenatis a condimentum vitae sapien. Tincidunt id aliquet risus feugiat in ante metus. Elementum nibh tellus molestie nunc non blandit massa enim. Ac tortor vitae purus faucibus ornare suspendisse. Pellentesque sit amet porttitor eget. Commodo quis imperdiet massa tincidunt. Nunc sed id semper risus in hendrerit gravida. Proin nibh nisl condimentum id venenatis a condimentum. Tortor at risus viverra adipiscing at in. Pharetra massa massa ultricies mi quis hendrerit dolor. Tempor id eu nisl nunc mi ipsum faucibus vitae.

-

Viverra mauris in aliquam sem. Viverra tellus in hac habitasse platea. Facilisi nullam vehicula ipsum a arcu cursus. Nunc sed augue lacus viverra vitae congue eu. Pretium fusce id velit ut tortor pretium viverra suspendisse. Eu scelerisque felis imperdiet proin. Tincidunt arcu non sodales neque sodales ut etiam sit amet. Tellus at urna condimentum mattis pellentesque. Congue nisi vitae suscipit tellus. Ut morbi tincidunt augue interdum.

+

Tellus orci

-

Scelerisque in dictum non consectetur a. Elit pellentesque habitant morbi tristique senectus et. Nulla aliquet enim tortor at auctor urna nunc id. In ornare quam viverra orci. Auctor eu augue ut lectus arcu bibendum at varius vel. In cursus turpis massa tincidunt dui ut ornare lectus. Accumsan in nisl nisi scelerisque eu ultrices vitae auctor eu. A diam sollicitudin tempor id. Tellus mauris a diam maecenas sed enim ut sem. Pellentesque id nibh tortor id aliquet lectus proin. Fermentum et sollicitudin ac orci phasellus. Dolor morbi non arcu risus quis. Bibendum enim facilisis gravida neque. Tellus in metus vulputate eu scelerisque felis. Integer malesuada nunc vel risus commodo. Lacus laoreet non curabitur gravida arcu.

- `, - // Customize the count of widget divs, and customize the contents of them. As long as they're wrapped in the widget div, they should auto-wrap and maintain margins between themselves - widgets = ` -
-
- -
+

Viverra mauris in aliquam sem. Viverra tellus in hac habitasse platea. Facilisi nullam vehicula ipsum a arcu cursus. Nunc sed augue lacus viverra vitae congue eu. Pretium fusce id velit ut tortor pretium viverra suspendisse. Eu scelerisque felis imperdiet proin. Tincidunt arcu non sodales neque sodales ut etiam sit amet. Tellus at urna condimentum mattis pellentesque. Congue nisi vitae suscipit tellus. Ut morbi tincidunt augue interdum.

-
- -
- -
- -
-
- `, - // Which of the above variables to actually insert into the page - whichAccessoriesToUse = simpleAbout; +

Scelerisque in dictum non consectetur a. Elit pellentesque habitant morbi tristique senectus et. Nulla aliquet enim tortor at auctor urna nunc id. In ornare quam viverra orci. Auctor eu augue ut lectus arcu bibendum at varius vel. In cursus turpis massa tincidunt dui ut ornare lectus. Accumsan in nisl nisi scelerisque eu ultrices vitae auctor eu. A diam sollicitudin tempor id. Tellus mauris a diam maecenas sed enim ut sem. Pellentesque id nibh tortor id aliquet lectus proin. Fermentum et sollicitudin ac orci phasellus. Dolor morbi non arcu risus quis. Bibendum enim facilisis gravida neque. Tellus in metus vulputate eu scelerisque felis. Integer malesuada nunc vel risus commodo. Lacus laoreet non curabitur gravida arcu.

+`; +// Customize the count of widget divs, and customize the contents of them. As long as they're wrapped in the widget div, they should auto-wrap and maintain margins between themselves +const widgets = ` +
+
+ +
+
+ +
+
+ +
+
+`; +// Which of the above variables to actually insert into the page +const whichAccessoriesToUse = simpleAbout; // Configure external links to appear at the bottom of the page. Displayed only if externalLinksBar (above) is true const linkSets = [ - { - label: "IEM graph databases", - links: [ - { - name: "Audio Discourse", - url: "https://iems.audiodiscourse.com/" - }, - { - name: "Bad Guy", - url: "https://hbb.squig.link/" - }, - { - name: "Banbeucmas", - url: "https://banbeu.com/graph/tool/" - }, - { - name: "HypetheSonics", - url: "https://www.hypethesonics.com/iemdbc/" - }, - { - name: "In-Ear Fidelity", - url: "https://crinacle.com/graphs/iems/graphtool/" - }, - { - name: "Precogvision", - url: "https://precog.squig.link/" - }, - { - name: "Rikudou Goku", - url: "https://rg.squig.link/" - }, - { - name: "Super* Review", - url: "https://squig.link/" - }, - ] - }, - { - label: "Headphones", - links: [ - { - name: "Audio Discourse", - url: "https://headphones.audiodiscourse.com/" - }, - { - name: "In-Ear Fidelity", - url: "https://crinacle.com/graphs/headphones/graphtool/" - }, - { - name: "Super* Review", - url: "https://squig.link/hp.html" - } - ] - } + { + label: "IEM graph databases", + links: [ + { + name: "Audio Discourse", + url: "https://iems.audiodiscourse.com/", + }, + { + name: "Bad Guy", + url: "https://hbb.squig.link/", + }, + { + name: "Banbeucmas", + url: "https://banbeu.com/graph/tool/", + }, + { + name: "HypetheSonics", + url: "https://www.hypethesonics.com/iemdbc/", + }, + { + name: "In-Ear Fidelity", + url: "https://crinacle.com/graphs/iems/graphtool/", + }, + { + name: "Precogvision", + url: "https://precog.squig.link/", + }, + { + name: "Rikudou Goku", + url: "https://rg.squig.link/", + }, + { + name: "Super* Review", + url: "https://squig.link/", + }, + ], + }, + { + label: "Headphones", + links: [ + { + name: "Audio Discourse", + url: "https://headphones.audiodiscourse.com/", + }, + { + name: "In-Ear Fidelity", + url: "https://crinacle.com/graphs/headphones/graphtool/", + }, + { + name: "Super* Review", + url: "https://squig.link/hp.html", + }, + ], + }, ]; - - // Set up analytics function setupGraphAnalytics() { - if ( analyticsEnabled ) { - const pageHead = document.querySelector("head"), - graphAnalytics = document.createElement("script"), - graphAnalyticsSrc = "graphAnalytics.js"; - - graphAnalytics.setAttribute("src", graphAnalyticsSrc); - pageHead.append(graphAnalytics); - } + if (analyticsEnabled) { + const pageHead = document.querySelector("head"), + graphAnalytics = document.createElement("script"), + graphAnalyticsSrc = "graphAnalytics.js"; + + graphAnalytics.setAttribute("src", graphAnalyticsSrc); + pageHead.append(graphAnalytics); + } } setupGraphAnalytics(); - - // If alt_header is enabled, these are the items added to the header let headerLogoText = "CrinGraph", - headerLogoImgUrl = "", - headerLinks = [ + headerLogoImgUrl = "", + headerLinks = [ { - name: "Sample", - url: "https://sample.com" + name: "Sample", + url: "https://sample.com", }, { - name: "Sample External", - url: "https://sample.com", - external: true - } -]; - + name: "Sample External", + url: "https://sample.com", + external: true, + }, + ]; let tutorialDefinitions = [ - { - name: 'Sub bass', - width: '20.1%', - description: 'Lorem ipsum.' - }, - { - name: 'Mid bass', - width: '19.2%', - description: 'Lorem ipsum.' - }, - { - name: 'Lower midrange', - width: '17.4%', - description: 'Lorem ipsum.' - }, - { - name: 'Upper midrange', - width: "20%", - description: 'Lorem ipsum.' - }, - { - name: 'Presence region', - width: '6%', - description: 'Lorem ipsum.' - }, - { - name: 'Mid treble', - width: '7.3%', - description: 'Lorem ipsum.' - }, - { - name: 'Air', - width: '10%', - description: 'Lorem ipsum.' - } -] + { + name: "Sub bass", + width: "20.1%", + description: "Lorem ipsum.", + }, + { + name: "Mid bass", + width: "19.2%", + description: "Lorem ipsum.", + }, + { + name: "Lower midrange", + width: "17.4%", + description: "Lorem ipsum.", + }, + { + name: "Upper midrange", + width: "20%", + description: "Lorem ipsum.", + }, + { + name: "Presence region", + width: "6%", + description: "Lorem ipsum.", + }, + { + name: "Mid treble", + width: "7.3%", + description: "Lorem ipsum.", + }, + { + name: "Air", + width: "10%", + description: "Lorem ipsum.", + }, +]; + // Configure paths to extraEQ plugins here let extraEQplugins = [ - //'./devicePEQ/plugin.js' // Path to one or more "extraEQ" plugins + //'./devicePEQ/plugin.js' // Path to one or more "extraEQ" plugins ]; diff --git a/config_hp.js b/config_hp.js index 7c1dac0..71baa96 100644 --- a/config_hp.js +++ b/config_hp.js @@ -2,14 +2,15 @@ const DIR = "data_hp/"; // Add a watermark to the graph. function watermark(svg) { - let wm = svg.append("g") - .attr("transform", "translate("+(pad.l+W/2)+","+(pad.t+H/2-20)+")") - .attr("opacity",0.2); -// wm.append("image") -// .attrs({x:-64, y:-64, width:128, height:128, "xlink:href":URL}); - wm.append("text") - .attrs({x:0, y:40, "font-size":40, "text-anchor":"middle"}) - .text("sample graphs"); + let wm = svg + .append("g") + .attr("transform", "translate(" + (pad.l + W / 2) + "," + (pad.t + H / 2 - 20) + ")") + .attr("opacity", 0.2); + // wm.append("image") + // .attrs({x:-64, y:-64, width:128, height:128, "xlink:href":URL}); + wm.append("text") + .attrs({ x: 0, y: 40, "font-size": 40, "text-anchor": "middle" }) + .text("sample graphs"); } const max_channel_imbalance = 5; @@ -19,8 +20,8 @@ const num_samples = 5; const scale_smoothing = 0.2; const targets = [ - { type:"Neutral" , files:["IEF Neutral"] }, - { type:"Preference", files:["IEF Harman"] } + { type: "Neutral", files: ["IEF Neutral"] }, + { type: "Preference", files: ["IEF Harman"] }, ]; // const init_phones = [ "HD650", "IEF Neutral Target" ]; diff --git a/data/phone_book.json b/data/phone_book.json index dd76dc9..e1a7ef0 100644 --- a/data/phone_book.json +++ b/data/phone_book.json @@ -2,105 +2,86 @@ { "name": "Demo", "phones": [ - {"name":["Variations"], - "file":["Combo Variant 1", - "Combo Variant 2", - "Combo Variant 3", - "Combo Variant 4"], - "suffix":["var1", - "var2", - "var3", - "var4"], - "reviewScore":"4", - "reviewLink":"https://www.youtube.com/", - "shopLink":"https://www.amazon.com/", - "price":"¥100" - }, - {"name":["Variations 2"], - "file":["Combo Variant 1", - "Combo Variant 2", - "Combo Variant 3", - "Combo Variant 4"], - "suffix":["var1", - "var2", - "var3", - "var4"], - "reviewScore":"A+", - "reviewLink":"https://www.head-fi.org/forums/", - "shopLink":"https://www.aliexpress.com/", - "price":"$100" - } + { + "name": ["Variations"], + "file": ["Combo Variant 1", "Combo Variant 2", "Combo Variant 3", "Combo Variant 4"], + "suffix": ["var1", "var2", "var3", "var4"], + "reviewScore": "4", + "reviewLink": "https://www.youtube.com/", + "shopLink": "https://www.amazon.com/", + "price": "¥100" + }, + { + "name": ["Variations 2"], + "file": ["Combo Variant 1", "Combo Variant 2", "Combo Variant 3", "Combo Variant 4"], + "suffix": ["var1", "var2", "var3", "var4"], + "reviewScore": "A+", + "reviewLink": "https://www.head-fi.org/forums/", + "shopLink": "https://www.aliexpress.com/", + "price": "$100" + } ] }, { - "name": "QBK", - "phones": [ "FQQ" - , "Q53" - , "KT" - , "B27" - , "BKF" - , "H2" - , "HH" - , "E05" - , "QK9" ] - }, { - "name": "ZKZ", + "name": "QBK", + "phones": ["FQQ", "Q53", "KT", "B27", "BKF", "H2", "HH", "E05", "QK9"] + }, + { + "name": "ZKZ", "suffix": "Sound", - "phones": [ "H4" - , "HHH" - , "KTK" - , {"name":"E10 Universal","file":"E10"} - , "EFZ9" - , {"name":"THT02 Extreme","file":"THT02"} - , "ZK" - , "E40" ] - }, { - "name": "RBG", + "phones": [ + "H4", + "HHH", + "KTK", + { "name": "E10 Universal", "file": "E10" }, + "EFZ9", + { "name": "THT02 Extreme", "file": "THT02" }, + "ZK", + "E40" + ] + }, + { + "name": "RBG", "suffix": "(Not an acronym)", - "phones": [ "B9" - , "BE0" ] - }, { - "name": "KQQ", - "phones": [ "QE" - , "K" - , "H" - , "QH19" - , {"name":"T4 Reference","file":"T4"} ] - }, { - "name": "KRKG", + "phones": ["B9", "BE0"] + }, + { + "name": "KQQ", + "phones": ["QE", "K", "H", "QH19", { "name": "T4 Reference", "file": "T4" }] + }, + { + "name": "KRKG", "suffix": "Ears", - "phones": [ "E5" - , "KK16" - , {"name":"RG Pro Edition","file":"RG"} - , "T0" - , "KH" - , "K5" ] - }, { - "name": "CQ", - "phones": [ "TC4" - , "Q2" - , {"name":"CE Reference","file":"CE"} - , {"name":"E04 Enhanced Bass","file":"E04"} - , "T" - , {"name":"TE66 Deluxe","file":"TE66"} - , "QHC" ] - }, { - "name": "GG", + "phones": ["E5", "KK16", { "name": "RG Pro Edition", "file": "RG" }, "T0", "KH", "K5"] + }, + { + "name": "CQ", + "phones": [ + "TC4", + "Q2", + { "name": "CE Reference", "file": "CE" }, + { "name": "E04 Enhanced Bass", "file": "E04" }, + "T", + { "name": "TE66 Deluxe", "file": "TE66" }, + "QHC" + ] + }, + { + "name": "GG", "suffix": "Hi-fi", - "phones": [ "T92" - , {"name":"H0 Pro","file":"H0"} - , "TG" - , "GGE" - , "HG7" ] - }, { - "name": "GZR", - "phones": [ {"name":"GRR Light","file":"GRR"} - , "F0" - , "HR" - , "HRH" - , {"name":"HTH Performance","file":"HTH"} - , "Z" - , "HTH67" - , "HT5" ] + "phones": ["T92", { "name": "H0 Pro", "file": "H0" }, "TG", "GGE", "HG7"] + }, + { + "name": "GZR", + "phones": [ + { "name": "GRR Light", "file": "GRR" }, + "F0", + "HR", + "HRH", + { "name": "HTH Performance", "file": "HTH" }, + "Z", + "HTH67", + "HT5" + ] } ] diff --git a/data_hp/phone_book.json b/data_hp/phone_book.json index 3e68ad5..31844fd 100644 --- a/data_hp/phone_book.json +++ b/data_hp/phone_book.json @@ -1,32 +1,32 @@ [ { - "name": "Auditory", + "name": "Auditory", "suffix": "Technology", - "phones": [ "tuT0" - , "u AX" - , "uiD11" - , "uddd" - , "yXir2" - , "tu" ] - }, { - "name": "Groot", + "phones": ["tuT0", "u AX", "uiD11", "uddd", "yXir2", "tu"] + }, + { + "name": "Groot", "suffix": "Sound", - "phones": [ "D D" - , "AA01" - , {"name":"oDH74 Deluxe","file":"oDH74"} - , {"name":"Gr Gamergate","file":"Gr"} - , "GArG5" ] - }, { - "name": "Himmler", + "phones": [ + "D D", + "AA01", + { "name": "oDH74 Deluxe", "file": "oDH74" }, + { "name": "Gr Gamergate", "file": "Gr" }, + "GArG5" + ] + }, + { + "name": "Himmler", "suffix": "Designs", - "phones": [ "AXm" - , {"name":"T D04 Reference","file":"T D04"} - , "rTA5" - , {"name":"iH-r Bass Style","file":"iH-r"} ] - }, { - "name": "Stonks", - "phones": [ {"name":"noS4 Pro","file":"noS4"} - , "kt0" - , "XoX" ] + "phones": [ + "AXm", + { "name": "T D04 Reference", "file": "T D04" }, + "rTA5", + { "name": "iH-r Bass Style", "file": "iH-r" } + ] + }, + { + "name": "Stonks", + "phones": [{ "name": "noS4 Pro", "file": "noS4" }, "kt0", "XoX"] } ] diff --git a/equalizer.js b/equalizer.js index a306b4e..ee30e15 100644 --- a/equalizer.js +++ b/equalizer.js @@ -4,405 +4,427 @@ https://github.com/jaakkopasanen/AutoEq/blob/master/biquad.py https://github.com/mohayonao/biquad-coeffs/tree/master/packages/biquad-coeffs-cookbook */ -Equalizer = (function() { - let config = { - // Change sample rate will affect the curve of filters close to nyquist frequency - // Here I choosed a common used value, but not all DSP software use this sample rate for EQ - DefaultSampleRate: 48000, - // AutoEQ will avoid filters above this frequency at first batch - TrebleStartFrom: 7000, - // Avoid filters close to nyquist frequency by default, because the behavior is implementation dependent - // https://github.com/jaakkopasanen/AutoEq/issues/240 - // https://github.com/jaakkopasanen/AutoEq/issues/411 - AutoEQRange: [20, 15000], - // Minimum and maximum Q for AutoEQ feature - OptimizeQRange: [0.5, 2], - // Minimum and maximum Gain for AutoEQ feature - OptimizeGainRange: [-12, 12], - // Delta and step of Freq, Q and Gain used for AutoEQ optimizing - OptimizeDeltas: [ - [10, 10, 10, 5, 0.1, 0.5], - [10, 10, 10, 2, 0.1, 0.2], - [10, 10, 10, 1, 0.1, 0.1], - ], - // Use to get response diff by EQ before smoothing - GraphicEQRawFrequences: ( // ~= 1/96 octave - new Array(Math.ceil(Math.log(20000 / 20) / Math.log(1.0072))).fill(null) - .map((_, i) => 20 * Math.pow(1.0072, i))), - // Smoothed 127 bands frequencies for graphic eq (wavelet) - GraphicEQFrequences: Array.from(new Set( - new Array(Math.ceil(Math.log(20000 / 20) / Math.log(1.0563))).fill(null) - .map((_, i) => Math.floor(20 * Math.pow(1.0563, i))))).sort((a, b) => a - b) - }; +Equalizer = (function () { + let config = { + // Change sample rate will affect the curve of filters close to nyquist frequency + // Here I choosed a common used value, but not all DSP software use this sample rate for EQ + DefaultSampleRate: 48000, + // AutoEQ will avoid filters above this frequency at first batch + TrebleStartFrom: 7000, + // Avoid filters close to nyquist frequency by default, because the behavior is implementation dependent + // https://github.com/jaakkopasanen/AutoEq/issues/240 + // https://github.com/jaakkopasanen/AutoEq/issues/411 + AutoEQRange: [20, 15000], + // Minimum and maximum Q for AutoEQ feature + OptimizeQRange: [0.5, 2], + // Minimum and maximum Gain for AutoEQ feature + OptimizeGainRange: [-12, 12], + // Delta and step of Freq, Q and Gain used for AutoEQ optimizing + OptimizeDeltas: [ + [10, 10, 10, 5, 0.1, 0.5], + [10, 10, 10, 2, 0.1, 0.2], + [10, 10, 10, 1, 0.1, 0.1], + ], + // Use to get response diff by EQ before smoothing + // ~= 1/96 octave + GraphicEQRawFrequences: new Array(Math.ceil(Math.log(20000 / 20) / Math.log(1.0072))) + .fill(null) + .map((_, i) => 20 * Math.pow(1.0072, i)), + // Smoothed 127 bands frequencies for graphic eq (wavelet) + GraphicEQFrequences: Array.from( + new Set( + new Array(Math.ceil(Math.log(20000 / 20) / Math.log(1.0563))) + .fill(null) + .map((_, i) => Math.floor(20 * Math.pow(1.0563, i))), + ), + ).sort((a, b) => a - b), + }; - let interp = function (fv, fr) { - let i = 0; - return fv.map(f => { - for (; i < fr.length-1; ++i) { - let [f0, v0] = fr[i]; - let [f1, v1] = fr[i+1]; - if (i == 0 && f < f0) { - return [f, v0]; - } else if (f >= f0 && f < f1) { - let v = v0 + (v1 - v0) * (f - f0) / (f1 - f0); - return [f, v]; - } - } - return [f, fr[fr.length-1][1]]; - }); - }; - - let lowshelf = function (freq, q, gain, sampleRate) { - freq = freq / (sampleRate || config.DefaultSampleRate); - freq = Math.max(1e-6, Math.min(freq, 1)); - q = Math.max(1e-4, Math.min(q, 1000)); - gain = Math.max(-40, Math.min(gain, 40)); + let interp = function (fv, fr) { + let i = 0; + return fv.map((f) => { + for (; i < fr.length - 1; ++i) { + let [f0, v0] = fr[i]; + let [f1, v1] = fr[i + 1]; + if (i == 0 && f < f0) { + return [f, v0]; + } else if (f >= f0 && f < f1) { + let v = v0 + ((v1 - v0) * (f - f0)) / (f1 - f0); + return [f, v]; + } + } + return [f, fr[fr.length - 1][1]]; + }); + }; - let w0 = 2 * Math.PI * freq; - let sin = Math.sin(w0); - let cos = Math.cos(w0); - let a = Math.pow(10, (gain / 40)); - let alpha = sin / (2 * q); - let alphamod = (2 * Math.sqrt(a) * alpha) || 0; + let lowshelf = function (freq, q, gain, sampleRate) { + freq = freq / (sampleRate || config.DefaultSampleRate); + freq = Math.max(1e-6, Math.min(freq, 1)); + q = Math.max(1e-4, Math.min(q, 1000)); + gain = Math.max(-40, Math.min(gain, 40)); - let a0 = ((a+1) + (a-1) * cos + alphamod); - let a1 = -2 * ((a-1) + (a+1) * cos ); - let a2 = ((a+1) + (a-1) * cos - alphamod); - let b0 = a * ((a+1) - (a-1) * cos + alphamod); - let b1 = 2 * a * ((a-1) - (a+1) * cos ); - let b2 = a * ((a+1) - (a-1) * cos - alphamod); + let w0 = 2 * Math.PI * freq; + let sin = Math.sin(w0); + let cos = Math.cos(w0); + let a = Math.pow(10, gain / 40); + let alpha = sin / (2 * q); + let alphamod = 2 * Math.sqrt(a) * alpha || 0; - return [ 1.0, a1/a0, a2/a0, b0/a0, b1/a0, b2/a0 ]; - }; + let a0 = a + 1 + (a - 1) * cos + alphamod; + let a1 = -2 * (a - 1 + (a + 1) * cos); + let a2 = a + 1 + (a - 1) * cos - alphamod; + let b0 = a * (a + 1 - (a - 1) * cos + alphamod); + let b1 = 2 * a * (a - 1 - (a + 1) * cos); + let b2 = a * (a + 1 - (a - 1) * cos - alphamod); - let highshelf = function (freq, q, gain, sampleRate) { - freq = freq / (sampleRate || config.DefaultSampleRate); - freq = Math.max(1e-6, Math.min(freq, 1)); - q = Math.max(1e-4, Math.min(q, 1000)); - gain = Math.max(-40, Math.min(gain, 40)); + return [1.0, a1 / a0, a2 / a0, b0 / a0, b1 / a0, b2 / a0]; + }; - let w0 = 2 * Math.PI * freq; - let sin = Math.sin(w0); - let cos = Math.cos(w0); - let a = Math.pow(10, (gain / 40)); - let alpha = sin / (2 * q); - let alphamod = (2 * Math.sqrt(a) * alpha) || 0; + let highshelf = function (freq, q, gain, sampleRate) { + freq = freq / (sampleRate || config.DefaultSampleRate); + freq = Math.max(1e-6, Math.min(freq, 1)); + q = Math.max(1e-4, Math.min(q, 1000)); + gain = Math.max(-40, Math.min(gain, 40)); - let a0 = ((a+1) - (a-1) * cos + alphamod); - let a1 = 2 * ((a-1) - (a+1) * cos ); - let a2 = ((a+1) - (a-1) * cos - alphamod); - let b0 = a * ((a+1) + (a-1) * cos + alphamod); - let b1 = -2 * a * ((a-1) + (a+1) * cos ); - let b2 = a * ((a+1) + (a-1) * cos - alphamod); + let w0 = 2 * Math.PI * freq; + let sin = Math.sin(w0); + let cos = Math.cos(w0); + let a = Math.pow(10, gain / 40); + let alpha = sin / (2 * q); + let alphamod = 2 * Math.sqrt(a) * alpha || 0; - return [ 1.0, a1/a0, a2/a0, b0/a0, b1/a0, b2/a0 ]; - }; + let a0 = a + 1 - (a - 1) * cos + alphamod; + let a1 = 2 * (a - 1 - (a + 1) * cos); + let a2 = a + 1 - (a - 1) * cos - alphamod; + let b0 = a * (a + 1 + (a - 1) * cos + alphamod); + let b1 = -2 * a * (a - 1 + (a + 1) * cos); + let b2 = a * (a + 1 + (a - 1) * cos - alphamod); - let peaking = function (freq, q, gain, sampleRate) { - freq = freq / (sampleRate || config.DefaultSampleRate); - freq = Math.max(1e-6, Math.min(freq, 1)); - q = Math.max(1e-4, Math.min(q, 1000)); - gain = Math.max(-40, Math.min(gain, 40)); + return [1.0, a1 / a0, a2 / a0, b0 / a0, b1 / a0, b2 / a0]; + }; - let w0 = 2 * Math.PI * freq; - let sin = Math.sin(w0); - let cos = Math.cos(w0); - let a = Math.pow(10, (gain / 40)); - let alpha = sin / (2 * q); + let peaking = function (freq, q, gain, sampleRate) { + freq = freq / (sampleRate || config.DefaultSampleRate); + freq = Math.max(1e-6, Math.min(freq, 1)); + q = Math.max(1e-4, Math.min(q, 1000)); + gain = Math.max(-40, Math.min(gain, 40)); - let a0 = 1 + alpha / a; - let a1 = -2 * cos; - let a2 = 1 - alpha / a; - let b0 = 1 + alpha * a; - let b1 = -2 * cos; - let b2 = 1 - alpha * a; + let w0 = 2 * Math.PI * freq; + let sin = Math.sin(w0); + let cos = Math.cos(w0); + let a = Math.pow(10, gain / 40); + let alpha = sin / (2 * q); - return [ 1.0, a1/a0, a2/a0, b0/a0, b1/a0, b2/a0 ]; - }; + let a0 = 1 + alpha / a; + let a1 = -2 * cos; + let a2 = 1 - alpha / a; + let b0 = 1 + alpha * a; + let b1 = -2 * cos; + let b2 = 1 - alpha * a; - let calc_gains = function (freqs, coeffs, sampleRate) { - sampleRate = sampleRate || config.DefaultSampleRate; - let gains = new Array(freqs.length).fill(0); + return [1.0, a1 / a0, a2 / a0, b0 / a0, b1 / a0, b2 / a0]; + }; - for (let i = 0; i < coeffs.length; ++i) { - let [ a0, a1, a2, b0, b1, b2] = coeffs[i]; - for (let j = 0; j < freqs.length; ++j) { - let w = 2 * Math.PI * freqs[j] / sampleRate; - let phi = 4 * Math.pow(Math.sin(w / 2), 2); - let c = ( - 10 * Math.log10(Math.pow(b0 + b1 + b2, 2) + - (b0 * b2 * phi - (b1 * (b0 + b2) + 4 * b0 * b2)) * phi) - - 10 * Math.log10(Math.pow(a0 + a1 + a2, 2) + - (a0 * a2 * phi - (a1 * (a0 + a2) + 4 * a0 * a2)) * phi)); - gains[j] += c; - } - } - return gains; - }; + let calc_gains = function (freqs, coeffs, sampleRate) { + sampleRate = sampleRate || config.DefaultSampleRate; + let gains = new Array(freqs.length).fill(0); - let calc_preamp = function (fr1, fr2) { - let maxGain = -Infinity; - for (let i = 0; i < fr1.length; ++i) { - maxGain = Math.max(maxGain, fr2[i][1] - fr1[i][1]); - } - return -maxGain; - }; + for (let i = 0; i < coeffs.length; ++i) { + let [a0, a1, a2, b0, b1, b2] = coeffs[i]; + for (let j = 0; j < freqs.length; ++j) { + let w = (2 * Math.PI * freqs[j]) / sampleRate; + let phi = 4 * Math.pow(Math.sin(w / 2), 2); + let c = + 10 * + Math.log10( + Math.pow(b0 + b1 + b2, 2) + (b0 * b2 * phi - (b1 * (b0 + b2) + 4 * b0 * b2)) * phi, + ) - + 10 * + Math.log10( + Math.pow(a0 + a1 + a2, 2) + (a0 * a2 * phi - (a1 * (a0 + a2) + 4 * a0 * a2)) * phi, + ); + gains[j] += c; + } + } + return gains; + }; - let calc_distance = function (fr1, fr2) { - let distance = 0; - for (let i = 0; i < fr1.length; ++i) { - let d = Math.abs(fr1[i][1] - fr2[i][1]); - distance += (d >= 0.1 ? d : 0); - } - return distance / fr1.length; - }; + let calc_preamp = function (fr1, fr2) { + let maxGain = -Infinity; + for (let i = 0; i < fr1.length; ++i) { + maxGain = Math.max(maxGain, fr2[i][1] - fr1[i][1]); + } + return -maxGain; + }; - let filters_to_coeffs = function (filters, sampleRate) { - return filters.map(f => { - if (!f.freq || !f.gain || !f.q) { - return null; - } else if (f.type === "LSQ") { - return lowshelf(f.freq, f.q, f.gain, sampleRate); - } else if (f.type === "HSQ") { - return highshelf(f.freq, f.q, f.gain, sampleRate); - } else if (f.type === "PK") { - return peaking(f.freq, f.q, f.gain, sampleRate); - } - return null; - }).filter(f => f); - }; + let calc_distance = function (fr1, fr2) { + let distance = 0; + for (let i = 0; i < fr1.length; ++i) { + let d = Math.abs(fr1[i][1] - fr2[i][1]); + distance += d >= 0.1 ? d : 0; + } + return distance / fr1.length; + }; - let apply = function (fr, filters, sampleRate) { - let freqs = new Array(fr.length).fill(null); - for (let i = 0; i < fr.length; ++i) { - freqs[i] = fr[i][0]; + let filters_to_coeffs = function (filters, sampleRate) { + return filters + .map((f) => { + if (!f.freq || !f.gain || !f.q) { + return null; + } else if (f.type === "LSQ") { + return lowshelf(f.freq, f.q, f.gain, sampleRate); + } else if (f.type === "HSQ") { + return highshelf(f.freq, f.q, f.gain, sampleRate); + } else if (f.type === "PK") { + return peaking(f.freq, f.q, f.gain, sampleRate); } - let coeffs = filters_to_coeffs(filters, sampleRate); - let gains = calc_gains(freqs, coeffs, sampleRate); - let fr_eq = new Array(fr.length).fill(null); - for (let i = 0; i < fr.length; ++i) { - fr_eq[i] = [fr[i][0], fr[i][1] + gains[i]]; - } - return fr_eq; - }; + return null; + }) + .filter((f) => f); + }; - let as_graphic_eq = function (filters, sampleRate) { - let rawFS = config.GraphicEQRawFrequences, fs = config.GraphicEQFrequences; - let coeffs = filters_to_coeffs(filters, sampleRate); - let gains = calc_gains(rawFS, coeffs, sampleRate); - let rawFR = rawFS.map((f, i) => [f, gains[i]]); - // Interpolate and smoothing with moving average - let i = 0; - let resultFR = fs.map((f, j) => { - let freqTo = (j < fs.length-1) ? Math.sqrt(f * fs[j+1]) : 20000; - let points = []; - for (; i < rawFS.length; ++i) { - if (rawFS[i] < freqTo) { - points.push(rawFR[i][1]); - } else { - break - } - } - let avg = points.reduce((a, b) => a + b, 0) / points.length; - return [f, avg]; - }); - // Normalize (apply preamp) - let maxGain = resultFR.reduce((a, b) => a > b[1] ? a : b[1], -Infinity); - resultFR = resultFR.map(([f, v]) => [f, v-maxGain]); - return resultFR; - }; + let apply = function (fr, filters, sampleRate) { + let freqs = new Array(fr.length).fill(null); + for (let i = 0; i < fr.length; ++i) { + freqs[i] = fr[i][0]; + } + let coeffs = filters_to_coeffs(filters, sampleRate); + let gains = calc_gains(freqs, coeffs, sampleRate); + let fr_eq = new Array(fr.length).fill(null); + for (let i = 0; i < fr.length; ++i) { + fr_eq[i] = [fr[i][0], fr[i][1] + gains[i]]; + } + return fr_eq; + }; - let search_candidates = function (fr, frTarget, threshold) { - let state = 0; // 1: peak, 0: matched, -1: dip - let startIndex = -1; - let candidates = []; - let [minFreq, maxFreq] = config.AutoEQRange; - for (let i = 0; i < fr.length; ++i) { - let [f, v0] = fr[i]; - let v1 = frTarget[i][1]; - let delta = v0 - v1; - let deltaAbs = Math.abs(delta); - let nextState = (deltaAbs < threshold) ? 0 : (delta / deltaAbs); - if (nextState === state) { - continue; - } - if (startIndex >= 0) { - if (state != 0) { - let start = fr[startIndex][0]; - let end = f; - let center = Math.sqrt(start * end); - let gain = ( - interp([center], frTarget.slice(startIndex, i))[0][1] - - interp([center], fr.slice(startIndex, i))[0][1]); - let q = center / (end - start); - if (center >= minFreq && center <= maxFreq) { - candidates.push({ type: "PK", freq: center, q, gain }); - } - } - startIndex = -1; - } else { - startIndex = i; - } - state = nextState; + let as_graphic_eq = function (filters, sampleRate) { + let rawFS = config.GraphicEQRawFrequences, + fs = config.GraphicEQFrequences; + let coeffs = filters_to_coeffs(filters, sampleRate); + let gains = calc_gains(rawFS, coeffs, sampleRate); + let rawFR = rawFS.map((f, i) => [f, gains[i]]); + // Interpolate and smoothing with moving average + let i = 0; + let resultFR = fs.map((f, j) => { + let freqTo = j < fs.length - 1 ? Math.sqrt(f * fs[j + 1]) : 20000; + let points = []; + for (; i < rawFS.length; ++i) { + if (rawFS[i] < freqTo) { + points.push(rawFR[i][1]); + } else { + break; } - return candidates; - }; + } + let avg = points.reduce((a, b) => a + b, 0) / points.length; + return [f, avg]; + }); + // Normalize (apply preamp) + let maxGain = resultFR.reduce((a, b) => (a > b[1] ? a : b[1]), -Infinity); + resultFR = resultFR.map(([f, v]) => [f, v - maxGain]); + return resultFR; + }; - let freq_unit = function (freq) { - if (freq < 100) { - return 1; - } else if (freq < 1000) { - return 10; - } else if (freq < 10000) { - return 100; + let search_candidates = function (fr, frTarget, threshold) { + let state = 0; // 1: peak, 0: matched, -1: dip + let startIndex = -1; + let candidates = []; + let [minFreq, maxFreq] = config.AutoEQRange; + for (let i = 0; i < fr.length; ++i) { + let [f, v0] = fr[i]; + let v1 = frTarget[i][1]; + let delta = v0 - v1; + let deltaAbs = Math.abs(delta); + let nextState = deltaAbs < threshold ? 0 : delta / deltaAbs; + if (nextState === state) { + continue; + } + if (startIndex >= 0) { + if (state != 0) { + let start = fr[startIndex][0]; + let end = f; + let center = Math.sqrt(start * end); + let gain = + interp([center], frTarget.slice(startIndex, i))[0][1] - + interp([center], fr.slice(startIndex, i))[0][1]; + let q = center / (end - start); + if (center >= minFreq && center <= maxFreq) { + candidates.push({ type: "PK", freq: center, q, gain }); + } } - return 1000; - }; + startIndex = -1; + } else { + startIndex = i; + } + state = nextState; + } + return candidates; + }; - let strip = function (filters) { - // Make freq, q and gain look better and more compatible to some DSP device - let [minQ, maxQ] = config.OptimizeQRange; - let [minGain, maxGain] = config.OptimizeGainRange; - return filters.map(f => ({ - type: f.type, - freq: Math.floor(f.freq - f.freq % freq_unit(f.freq)), - q: Math.min(Math.max(Math.floor(f.q * 10) / 10, minQ), maxQ), - gain: Math.min(Math.max(Math.floor(f.gain * 10) / 10, minGain), maxGain) - })); - }; + let freq_unit = function (freq) { + if (freq < 100) { + return 1; + } else if (freq < 1000) { + return 10; + } else if (freq < 10000) { + return 100; + } + return 1000; + }; - let optimize = function (fr, frTarget, filters, iteration, dir) { - filters = strip(filters); - let combinations = []; - let [minFreq, maxFreq] = config.AutoEQRange; - let [minQ, maxQ] = config.OptimizeQRange; - let [minGain, maxGain] = config.OptimizeGainRange; - let [maxDF, maxDQ, maxDG, stepDF, stepDQ, stepDG] = ( - config.OptimizeDeltas[iteration]); - let [begin, end, step] = (dir ? - [filters.length-1, -1, -1] : [0, filters.length, 1]); - // Optimize freq, q, gain - for (let i = begin; i != end; i += step) { - let f = filters[i]; - let fr1 = apply(fr, filters.filter((f, fi) => fi !== i)); - let fr2 = apply(fr1, [f]); - let fr3 = apply(fr, filters); - let bestFilter = f; - let bestDistance = calc_distance(fr2, frTarget); - let testNewFilter = (df, dq, dg) => { - let freq = f.freq + df * freq_unit(f.freq) * stepDF; - let q = f.q + dq * stepDQ; - let gain = f.gain + dg * stepDG; - if (freq < minFreq || freq > maxFreq || q < minQ || - q > maxQ || gain < minGain || gain > maxGain) { - return false; - } - let newFilter = { type: f.type, freq, q, gain }; - let newFR = apply(fr1, [newFilter]); - let newDistance = calc_distance(newFR, frTarget); - if (newDistance < bestDistance) { - bestFilter = newFilter; - bestDistance = newDistance; - return true; - } - return false; - } - for (let df = -maxDF; df < maxDF; ++df) { - // Use smaller Q as possible - for (let dq = maxDQ-1; dq >= -maxDQ; --dq) { - for (let dg = 1; dg < maxDG; ++dg) { - if (!testNewFilter(df, dq, dg)) { - break; - } - } - for (let dg = -1; dg >= -maxDG; --dg) { - if (!testNewFilter(df, dq, dg)) { - break; - } - } - } - } - filters[i] = bestFilter; + let strip = function (filters) { + // Make freq, q and gain look better and more compatible to some DSP device + let [minQ, maxQ] = config.OptimizeQRange; + let [minGain, maxGain] = config.OptimizeGainRange; + return filters.map((f) => ({ + type: f.type, + freq: Math.floor(f.freq - (f.freq % freq_unit(f.freq))), + q: Math.min(Math.max(Math.floor(f.q * 10) / 10, minQ), maxQ), + gain: Math.min(Math.max(Math.floor(f.gain * 10) / 10, minGain), maxGain), + })); + }; + + let optimize = function (fr, frTarget, filters, iteration, dir) { + filters = strip(filters); + let combinations = []; + let [minFreq, maxFreq] = config.AutoEQRange; + let [minQ, maxQ] = config.OptimizeQRange; + let [minGain, maxGain] = config.OptimizeGainRange; + let [maxDF, maxDQ, maxDG, stepDF, stepDQ, stepDG] = config.OptimizeDeltas[iteration]; + let [begin, end, step] = dir ? [filters.length - 1, -1, -1] : [0, filters.length, 1]; + // Optimize freq, q, gain + for (let i = begin; i != end; i += step) { + let f = filters[i]; + let fr1 = apply( + fr, + filters.filter((f, fi) => fi !== i), + ); + let fr2 = apply(fr1, [f]); + let fr3 = apply(fr, filters); + let bestFilter = f; + let bestDistance = calc_distance(fr2, frTarget); + let testNewFilter = (df, dq, dg) => { + let freq = f.freq + df * freq_unit(f.freq) * stepDF; + let q = f.q + dq * stepDQ; + let gain = f.gain + dg * stepDG; + if ( + freq < minFreq || + freq > maxFreq || + q < minQ || + q > maxQ || + gain < minGain || + gain > maxGain + ) { + return false; } - if (!dir) { - return optimize(fr, frTarget, filters, iteration, 1); - } else { - filters = filters.sort((a, b) => a.freq - b.freq); - // Merge closed filters - for (let i = 0; i < filters.length-1;) { - let f1 = filters[i]; - let f2 = filters[i+1]; - if (Math.abs(f1.freq - f2.freq) <= freq_unit(f1.freq) && - Math.abs(f1.q - f2.q) <= 0.1) { - f1.gain += f2.gain; - filters.splice(i+1, 1); - } else { - ++i; - } + let newFilter = { type: f.type, freq, q, gain }; + let newFR = apply(fr1, [newFilter]); + let newDistance = calc_distance(newFR, frTarget); + if (newDistance < bestDistance) { + bestFilter = newFilter; + bestDistance = newDistance; + return true; + } + return false; + }; + for (let df = -maxDF; df < maxDF; ++df) { + // Use smaller Q as possible + for (let dq = maxDQ - 1; dq >= -maxDQ; --dq) { + for (let dg = 1; dg < maxDG; ++dg) { + if (!testNewFilter(df, dq, dg)) { + break; } - // Remove unnecessary filters - let bestDistance = calc_distance(apply(fr, filters), frTarget); - for (let i = 0; i < filters.length;) { - if (Math.abs(filters[i].gain) <= 0.1) { - filters.splice(i, 1); - continue; - } - let newDistance = calc_distance(apply(fr, - filters.filter((f, fi) => fi !== i)), frTarget); - if (newDistance < bestDistance) { - filters.splice(i, 1); - bestDistance = newDistance; - } else { - ++i; - } + } + for (let dg = -1; dg >= -maxDG; --dg) { + if (!testNewFilter(df, dq, dg)) { + break; } - return filters; + } } - }; - - let autoeq = function (fr, frTarget, maxFilters) { - // 2 steps manual optimized algorithm - // fr, frTarget should has same resolution and normalized - let firstBatchSize = Math.max(Math.floor(maxFilters / 2) - 1, 1); - let firstCandidates = search_candidates(fr, frTarget, 1); - let firstFilters = (firstCandidates - // Dont adjust treble in the first batch - .filter(c => c.freq <= config.TrebleStartFrom) - // Wider bandwidth (smaller Q) come first - .sort((a, b) => a.q - b.q) - .slice(0, firstBatchSize) - .sort((a, b) => a.freq - b.freq)); - for (let i = 0; i < config.OptimizeDeltas.length; ++i) { - firstFilters = optimize(fr, frTarget, firstFilters, i); + } + filters[i] = bestFilter; + } + if (!dir) { + return optimize(fr, frTarget, filters, iteration, 1); + } else { + filters = filters.sort((a, b) => a.freq - b.freq); + // Merge closed filters + for (let i = 0; i < filters.length - 1; ) { + let f1 = filters[i]; + let f2 = filters[i + 1]; + if (Math.abs(f1.freq - f2.freq) <= freq_unit(f1.freq) && Math.abs(f1.q - f2.q) <= 0.1) { + f1.gain += f2.gain; + filters.splice(i + 1, 1); + } else { + ++i; } - let secondFR = apply(fr, firstFilters); - let secondBatchSize = maxFilters - firstFilters.length; - let secondCandidates = search_candidates(secondFR, frTarget, 0.5); - let secondFilters = (secondCandidates - .sort((a, b) => a.q - b.q) - .slice(0, secondBatchSize) - .sort((a, b) => a.freq - b.freq)); - for (let i = 0; i < config.OptimizeDeltas.length; ++i) { - secondFilters = optimize(secondFR, frTarget, secondFilters, i); + } + // Remove unnecessary filters + let bestDistance = calc_distance(apply(fr, filters), frTarget); + for (let i = 0; i < filters.length; ) { + if (Math.abs(filters[i].gain) <= 0.1) { + filters.splice(i, 1); + continue; } - let allFilters = firstFilters.concat(secondFilters); - for (let i = 0; i < config.OptimizeDeltas.length; ++i) { - allFilters = optimize(fr, frTarget, allFilters, i); + let newDistance = calc_distance( + apply( + fr, + filters.filter((f, fi) => fi !== i), + ), + frTarget, + ); + if (newDistance < bestDistance) { + filters.splice(i, 1); + bestDistance = newDistance; + } else { + ++i; } - return strip(allFilters); - }; + } + return filters; + } + }; - return { - config, - interp, - lowshelf, - highshelf, - peaking, - calc_gains, - calc_preamp, - apply, - as_graphic_eq, - autoeq + let autoeq = function (fr, frTarget, maxFilters) { + // 2 steps manual optimized algorithm + // fr, frTarget should has same resolution and normalized + let firstBatchSize = Math.max(Math.floor(maxFilters / 2) - 1, 1); + let firstCandidates = search_candidates(fr, frTarget, 1); + let firstFilters = firstCandidates + // Dont adjust treble in the first batch + .filter((c) => c.freq <= config.TrebleStartFrom) + // Wider bandwidth (smaller Q) come first + .sort((a, b) => a.q - b.q) + .slice(0, firstBatchSize) + .sort((a, b) => a.freq - b.freq); + for (let i = 0; i < config.OptimizeDeltas.length; ++i) { + firstFilters = optimize(fr, frTarget, firstFilters, i); } -})(); + let secondFR = apply(fr, firstFilters); + let secondBatchSize = maxFilters - firstFilters.length; + let secondCandidates = search_candidates(secondFR, frTarget, 0.5); + let secondFilters = secondCandidates + .sort((a, b) => a.q - b.q) + .slice(0, secondBatchSize) + .sort((a, b) => a.freq - b.freq); + for (let i = 0; i < config.OptimizeDeltas.length; ++i) { + secondFilters = optimize(secondFR, frTarget, secondFilters, i); + } + let allFilters = firstFilters.concat(secondFilters); + for (let i = 0; i < config.OptimizeDeltas.length; ++i) { + allFilters = optimize(fr, frTarget, allFilters, i); + } + return strip(allFilters); + }; + return { + config, + interp, + lowshelf, + highshelf, + peaking, + calc_gains, + calc_preamp, + apply, + as_graphic_eq, + autoeq, + }; +})(); diff --git a/graphAnalytics.js b/graphAnalytics.js index 2c7fef2..42c88a2 100644 --- a/graphAnalytics.js +++ b/graphAnalytics.js @@ -1,7 +1,5 @@ -let analyticsSite = "Generic Graph site", // Site name for attributing analytics events to your site - logAnalytics = true; // If true, events are logged in console - - +let analyticsSite = "Generic Graph site", // Site name for attributing analytics events to your site + logAnalytics = true; // If true, events are logged in console // ************************************************************* // Functions to fire events @@ -9,31 +7,55 @@ let analyticsSite = "Generic Graph site", // Site name for attributing // For events related to specific phones, e.g. when a phone is displayed function pushPhoneTag(eventName, p, trigger) { - let eventTrigger = trigger ? trigger : "user", - phoneBrand = p.dispBrand ? p.dispBrand : "Target", - phoneModel = p.dispName, - value = 1; - - // Write function here to push event with the values described above - - if (logAnalytics) { console.log("Event: "+ eventName +"\nTrigger: "+ eventTrigger +"\nSite: "+ analyticsSite +"\nPhone: "+ phoneBrand +" "+ phoneModel); } + let eventTrigger = trigger ? trigger : "user", + phoneBrand = p.dispBrand ? p.dispBrand : "Target", + phoneModel = p.dispName, + value = 1; + + // Write function here to push event with the values described above + + if (logAnalytics) { + console.log( + "Event: " + + eventName + + "\nTrigger: " + + eventTrigger + + "\nSite: " + + analyticsSite + + "\nPhone: " + + phoneBrand + + " " + + phoneModel, + ); + } } - - // For events not related to a specific phone, e.g. user clicked screenshot button function pushEventTag(eventName, targetWindow, trigger) { - let eventTrigger = trigger ? trigger : "user", - url = targetWindow.location.href, - par = "?share=", - value = 1, - activePhones = url.includes(par) ? decodeURI(url.replace(/_/g," ").split(par).pop().replace(/,/g, ", ")) : "null"; - - // Write function here to push event with the values described above - - if (logAnalytics) { console.log("Event: "+ eventName +"\nTrigger: "+ eventTrigger +"\nSite name: "+ analyticsSite +"\nActive: "+activePhones); } + let eventTrigger = trigger ? trigger : "user", + url = targetWindow.location.href, + par = "?share=", + value = 1, + activePhones = url.includes(par) + ? decodeURI(url.replace(/_/g, " ").split(par).pop().replace(/,/g, ", ")) + : "null"; + + // Write function here to push event with the values described above + + if (logAnalytics) { + console.log( + "Event: " + + eventName + + "\nTrigger: " + + eventTrigger + + "\nSite name: " + + analyticsSite + + "\nActive: " + + activePhones, + ); + } } - - -if (logAnalytics) { console.log("... Analytics initialized ... "); } \ No newline at end of file +if (logAnalytics) { + console.log("... Analytics initialized ... "); +} diff --git a/graph_free.html b/graph_free.html index af9a2f5..33c1288 100644 --- a/graph_free.html +++ b/graph_free.html @@ -1,25 +1,26 @@ - - + + - - - - - -
- -
- - - - - - - - - + + + + + +
+ +
+ + + + + + + + + diff --git a/graph_hp.html b/graph_hp.html index bcf46af..8575387 100644 --- a/graph_hp.html +++ b/graph_hp.html @@ -1,20 +1,20 @@ - - + + - - - - - -
- -
- - - - - - - - + + + + + +
+ +
+ + + + + + + + diff --git a/graphtool.js b/graphtool.js index bb24b5c..9e482fa 100644 --- a/graphtool.js +++ b/graphtool.js @@ -1,5 +1,6 @@ let doc = d3.select(".graphtool"); -doc.html(` +doc.html( + ` @@ -22,9 +23,15 @@ doc.html(`
-
+
- +
@@ -43,11 +50,15 @@ doc.html(`
Normalize:
- + dB
- + Hz
@@ -205,3137 +216,3681 @@ doc.html(` -`); +`, +); - -let pad = { l:15, r:15, t:10, b:36 }; -let W0 = 800, W = W0 - pad.l - pad.r, - H0 = 360, H = H0 - pad.t - pad.b; +let pad = { l: 15, r: 15, t: 10, b: 36 }; +let W0 = 800, + W = W0 - pad.l - pad.r, + H0 = 360, + H = H0 - pad.t - pad.b; let gr = doc.select("#fr-graph"), - defs = gr.append("defs"); - - -gr.append("rect").attrs({x:0, y:pad.t-8, width:W0, height:H0-22, rx:4, - "class":"graphBackground"}); + defs = gr.append("defs"); + +gr.append("rect").attrs({ + x: 0, + y: pad.t - 8, + width: W0, + height: H0 - 22, + rx: 4, + class: "graphBackground", +}); watermark(gr); - // Scales -let x = d3.scaleLog() - .domain([20,20000]) - .range([pad.l,pad.l+W]); +let x = d3 + .scaleLog() + .domain([20, 20000]) + .range([pad.l, pad.l + W]); -let yD = [29.5,85], // Decibels - yR = [pad.t+H,pad.t+10]; +let yD = [29.5, 85], // Decibels + yR = [pad.t + H, pad.t + 10]; let y = d3.scaleLinear().domain(yD).range(yR); - // y axis -defs.append("filter").attr("id","blur").attr("filterUnits","userSpaceOnUse") - .attrs({x:-W-4,y:-2,width:W+8,height:4}) - .append("feGaussianBlur").attr("in","SourceGraphic") - .attr("stdDeviation", 0.8); +defs + .append("filter") + .attr("id", "blur") + .attr("filterUnits", "userSpaceOnUse") + .attrs({ x: -W - 4, y: -2, width: W + 8, height: 4 }) + .append("feGaussianBlur") + .attr("in", "SourceGraphic") + .attr("stdDeviation", 0.8); let yAxis = d3.axisLeft(y).tickSize(W).tickSizeOuter(0).tickPadding(1); function fmtY(ya) { - let d = y.domain(), - r = d[1] - d[0], - t = r<40 ? 1 : 5, - y0= Math.ceil (d[0]/t), - y1= Math.floor(d[1]/t), - isMinor = t===5 ? (()=>false) : ((_,i)=>(y0+i)%5!==0); - yAxis.tickValues(d3.range(y1-y0+1).map(i=>t*(y0+i)))(ya); - ya.select(".domain").remove(); - ya.selectAll(".tick line") - .attr("stroke-linecap", "round") - .attrs((_,i) => { - let m = isMinor(_,i); - return { - filter: m ? null : "url(#blur)", - "stroke-width": m ? 0.2*(1-r/45) : 0.15*(1+45/r) - }; - }); - ya.selectAll(".tick text") - .attr("text-anchor","start") - .attr("x",-W+3) - .attr("dy",-2) - .filter(isMinor).attr("display","none"); -} -let yAxisObj = gr.append("g") - .attr("transform", "translate("+(pad.l+W)+",0)") - .call(fmtY); -yAxisObj.insert("text") - .attr("transform","rotate(-90)") - .attr("fill","currentColor") - .attr("text-anchor","end") - .attr("y",-W-2).attr("x",-pad.t) - .text("dB"); - + let d = y.domain(), + r = d[1] - d[0], + t = r < 40 ? 1 : 5, + y0 = Math.ceil(d[0] / t), + y1 = Math.floor(d[1] / t), + isMinor = t === 5 ? () => false : (_, i) => (y0 + i) % 5 !== 0; + yAxis.tickValues(d3.range(y1 - y0 + 1).map((i) => t * (y0 + i)))(ya); + ya.select(".domain").remove(); + ya.selectAll(".tick line") + .attr("stroke-linecap", "round") + .attrs((_, i) => { + let m = isMinor(_, i); + return { + filter: m ? null : "url(#blur)", + "stroke-width": m ? 0.2 * (1 - r / 45) : 0.15 * (1 + 45 / r), + }; + }); + ya.selectAll(".tick text") + .attr("text-anchor", "start") + .attr("x", -W + 3) + .attr("dy", -2) + .filter(isMinor) + .attr("display", "none"); +} +let yAxisObj = gr + .append("g") + .attr("transform", "translate(" + (pad.l + W) + ",0)") + .call(fmtY); +yAxisObj + .insert("text") + .attr("transform", "rotate(-90)") + .attr("fill", "currentColor") + .attr("text-anchor", "end") + .attr("y", -W - 2) + .attr("x", -pad.t) + .text("dB"); // x axis -let xvals = [2,3,4,5,6,8,10,15]; -let xAxis = d3.axisBottom(x) - .tickSize(H+3).tickSizeOuter(0) - .tickValues(d3.merge([1,2,3].map(e=>xvals.map(m=>m*Math.pow(10,e)))).concat([20000])) - .tickFormat(f => f>=1000 ? (f/1000)+"k" : f); - -let tickPattern = [3,0,0,1,0,0,2,0], - getTickType = i => i===0 || i===3*8 ? 4 : tickPattern[i%8], - tickThickness = [2,4,4,9,15].map(t=>t/10); +let xvals = [2, 3, 4, 5, 6, 8, 10, 15]; +let xAxis = d3 + .axisBottom(x) + .tickSize(H + 3) + .tickSizeOuter(0) + .tickValues(d3.merge([1, 2, 3].map((e) => xvals.map((m) => m * Math.pow(10, e)))).concat([20000])) + .tickFormat((f) => (f >= 1000 ? f / 1000 + "k" : f)); + +let tickPattern = [3, 0, 0, 1, 0, 0, 2, 0], + getTickType = (i) => (i === 0 || i === 3 * 8 ? 4 : tickPattern[i % 8]), + tickThickness = [2, 4, 4, 9, 15].map((t) => t / 10); function fmtX(xa) { - xAxis(xa); - (xa.selection ? xa.selection() : xa).select(".domain").remove(); - xa.selectAll(".tick line") - .attr("y1", 10) - .attr("y2", 312) - .attr("stroke", "#333") - .attr("stroke-width", (_,i) => tickThickness[getTickType(i)]); - xa.selectAll(".tick text").filter((_,i) => tickPattern[i%8] === 0) - .attr("font-size","86%") - .attr("font-weight","lighter"); - xa.select(".tick:last-of-type text") - .attr("dx",-5) - .text("20kHz"); - xa.select(".tick:first-of-type text") - .attr("dx",4) - .text("20Hz"); -} -defs.append("clipPath").attr("id","x-clip") - .append("rect").attrs({x:0, y:0, width:W0, height:H0}); -let xAxisObj = gr.append("g") - .attr("clip-path", "url(#x-clip)") - .attr("transform", "translate(0,"+pad.t+")") - .call(fmtX); - + xAxis(xa); + (xa.selection ? xa.selection() : xa).select(".domain").remove(); + xa.selectAll(".tick line") + .attr("y1", 10) + .attr("y2", 312) + .attr("stroke", "#333") + .attr("stroke-width", (_, i) => tickThickness[getTickType(i)]); + xa.selectAll(".tick text") + .filter((_, i) => tickPattern[i % 8] === 0) + .attr("font-size", "86%") + .attr("font-weight", "lighter"); + xa.select(".tick:last-of-type text").attr("dx", -5).text("20kHz"); + xa.select(".tick:first-of-type text").attr("dx", 4).text("20Hz"); +} +defs + .append("clipPath") + .attr("id", "x-clip") + .append("rect") + .attrs({ x: 0, y: 0, width: W0, height: H0 }); +let xAxisObj = gr + .append("g") + .attr("clip-path", "url(#x-clip)") + .attr("transform", "translate(0," + pad.t + ")") + .call(fmtX); // Plot line -defs.selectAll().data([0,1]).join("linearGradient") - .attrs({x1:0,y1:0, x2:1,y2:0}) - .attr("id", i=>"grad"+i) - .selectAll().data(i=>[i,1-i]).join("stop") - .attr("offset",(_,i)=>i) - .attr("stop-color",j=>["black","white"][j]); -let fW = 7, // Fade width - fWm= 30; // Width at an interior edge -let fade = defs.append("mask") - .attr("id", "graphFade") - .attr("maskUnits", "userSpaceOnUse") - .append("g").attr("transform", "translate("+pad.l+","+pad.t+")"); -fade.append("rect").attrs({ x:0, y:0, width:W, height:H, fill:"white" }); -let fadeEdge = fade.selectAll().data([0,1]).join("rect") - .attrs(i=>({ x:i?W-fW:0, width:fW, y:0,height:H, fill:"url(#grad"+i+")" })); -let line = d3.line() - .x(d=>x(d[0])) - .y(d=>y(d[1])) - .curve(d3.curveNatural); - +defs + .selectAll() + .data([0, 1]) + .join("linearGradient") + .attrs({ x1: 0, y1: 0, x2: 1, y2: 0 }) + .attr("id", (i) => "grad" + i) + .selectAll() + .data((i) => [i, 1 - i]) + .join("stop") + .attr("offset", (_, i) => i) + .attr("stop-color", (j) => ["black", "white"][j]); +let fW = 7, // Fade width + fWm = 30; // Width at an interior edge +let fade = defs + .append("mask") + .attr("id", "graphFade") + .attr("maskUnits", "userSpaceOnUse") + .append("g") + .attr("transform", "translate(" + pad.l + "," + pad.t + ")"); +fade.append("rect").attrs({ x: 0, y: 0, width: W, height: H, fill: "white" }); +let fadeEdge = fade + .selectAll() + .data([0, 1]) + .join("rect") + .attrs((i) => ({ x: i ? W - fW : 0, width: fW, y: 0, height: H, fill: "url(#grad" + i + ")" })); +let line = d3 + .line() + .x((d) => x(d[0])) + .y((d) => y(d[1])) + .curve(d3.curveNatural); // Range buttons let selectedRange = 3; // Full range -let ranges = [[20,400],[100,4000],[1000,20000], [20,20000]], - edgeWs = [[fW,fWm],[fWm,fWm],[fWm,fW],[fW,fW]]; +let ranges = [ + [20, 400], + [100, 4000], + [1000, 20000], + [20, 20000], + ], + edgeWs = [ + [fW, fWm], + [fWm, fWm], + [fWm, fW], + [fW, fW], + ]; let rangeSel = doc.select(".zoom").selectAll("button"); -rangeSel.on("click", function (_,i) { - let r = selectedRange, - s = selectedRange = r===i ? 3 : i; - rangeSel.classed("selected", (_,j)=>j===s); - x.domain(ranges[s]); - // More time to go between bass and treble - let dur = Math.min(r,s)===0 && Math.max(r,s)===2 ? 1100 : 700; - clearLabels(); - gpath.selectAll("path").transition().duration(dur).attr("d", drawLine); - let e = edgeWs[s]; - fadeEdge.transition().duration(dur).attrs(i=>({x:i?W-e[i]:0, width:e[i]})); - xAxisObj.transition().duration(dur).call(fmtX); +rangeSel.on("click", function (_, i) { + let r = selectedRange, + s = (selectedRange = r === i ? 3 : i); + rangeSel.classed("selected", (_, j) => j === s); + x.domain(ranges[s]); + // More time to go between bass and treble + let dur = Math.min(r, s) === 0 && Math.max(r, s) === 2 ? 1100 : 700; + clearLabels(); + gpath.selectAll("path").transition().duration(dur).attr("d", drawLine); + let e = edgeWs[s]; + fadeEdge + .transition() + .duration(dur) + .attrs((i) => ({ x: i ? W - e[i] : 0, width: e[i] })); + xAxisObj.transition().duration(dur).call(fmtX); }); - // y-axis scaler let dB = { - y: y(60), - h: 15, - H: y(60)-y(70), - min: pad.t, - max: pad.t+H, - tr: _ => "translate("+(pad.l-9)+","+dB.y+")" + y: y(60), + h: 15, + H: y(60) - y(70), + min: pad.t, + max: pad.t + H, + tr: (_) => "translate(" + (pad.l - 9) + "," + dB.y + ")", }; -dB.all = gr.append("g").attr("class","dBScaler"), -dB.trans = dB.all.append("g").attr("transform", dB.tr()), -dB.scale = dB.trans.append("g").attr("transform", "scale(1,1)"); -dB.scale.selectAll().data([-1,1]) - .join("path").attr("stroke","none") - .attr("d", function (s) { - function getPathPart(l) { - let v=l[0].toLowerCase()==="v"; - for (let i=2-v; i({x:i*2.8,y:-d,width:0.8,height:2*d,fill:"#bbb"})); +(dB.all = gr.append("g").attr("class", "dBScaler")), + (dB.trans = dB.all.append("g").attr("transform", dB.tr())), + (dB.scale = dB.trans.append("g").attr("transform", "scale(1,1)")); +dB.scale + .selectAll() + .data([-1, 1]) + .join("path") + .attr("stroke", "none") + .attr("d", function (s) { + function getPathPart(l) { + let v = l[0].toLowerCase() === "v"; + for (let i = 2 - v; i < l.length; i += 2) l[i] *= s; + return l[0] + l.slice(1).join(" "); + } + return [ + ["M", 9.9, -1], + ["V", dB.H], + ["h", -1], + ["l", -1, -1.5], + ["l", -2.1, 2], + ["h", -5.6], + ["v", -1.5], + ["q", 7, 2, 8, -7], + ["V", 29], + ["c", 1, -16, -10, -15, -10, -14], + ["V", -1], + ] + .map(getPathPart) + .join(""); + }); +dB.scale + .selectAll() + .data([10, 7, 13]) + .join("rect") + .attrs((d, i) => ({ x: i * 2.8, y: -d, width: 0.8, height: 2 * d, fill: "#bbb" })); function getDrag(fn) { - return d3.drag() - .on("drag",fn) - .on("start",function(){dB.all.classed("active",true );}) - .on("end" ,function(){dB.all.classed("active",false);}); -} -dB.mid = dB.all.append("rect") - .attrs({x:(pad.l-11),y:dB.y-dB.h,width:12,height:2*dB.h,opacity:0}) - .call(getDrag(function () { - dB.y = d3.event.y; - dB.y = Math.min(dB.y, dB.max-dB.h*(dB.H/15)); - dB.y = Math.max(dB.y, dB.min+dB.h*(dB.H/15)); - d3.select(this).attr("y",dB.y-dB.h); - dB.trans.attr("transform", dB.tr()); - dB.updatey(); - })); -dB.circ = dB.trans.selectAll().data([-1,1]).join("circle") - .attrs({cx:5,cy:s=>dB.H*s,r:7,opacity:0}) - .call(getDrag(function () { - let h = Math.max(30, Math.abs(d3.event.y)); - h = Math.min(h, Math.min(dB.max-dB.y, dB.y-dB.min)); - let sc = h/dB.H; - dB.circ.attr("cy",s=>h*s); - dB.scale.attr("transform", "scale(1,"+sc+")"); - dB.h = 15*sc; - dB.mid.attrs({y:dB.y-dB.h,height:2*dB.h}); - dB.updatey(); - })); -let yCenter = 60; -dB.updatey = function (dom) { - let d = l => l[1]-l[0]; - y.domain(yR.map(y=>yCenter+(y-dB.y)*(15/dB.h)*d(yD)/d(yR))); - yAxisObj.call(fmtY); - let getTr = o => o ? "translate(0,"+(y(o)-y(0))+")" : null; - clearLabels(); - gpath.selectAll("path").call(redrawLine); + return d3 + .drag() + .on("drag", fn) + .on("start", function () { + dB.all.classed("active", true); + }) + .on("end", function () { + dB.all.classed("active", false); + }); } - +dB.mid = dB.all + .append("rect") + .attrs({ x: pad.l - 11, y: dB.y - dB.h, width: 12, height: 2 * dB.h, opacity: 0 }) + .call( + getDrag(function () { + dB.y = d3.event.y; + dB.y = Math.min(dB.y, dB.max - dB.h * (dB.H / 15)); + dB.y = Math.max(dB.y, dB.min + dB.h * (dB.H / 15)); + d3.select(this).attr("y", dB.y - dB.h); + dB.trans.attr("transform", dB.tr()); + dB.updatey(); + }), + ); +dB.circ = dB.trans + .selectAll() + .data([-1, 1]) + .join("circle") + .attrs({ cx: 5, cy: (s) => dB.H * s, r: 7, opacity: 0 }) + .call( + getDrag(function () { + let h = Math.max(30, Math.abs(d3.event.y)); + h = Math.min(h, Math.min(dB.max - dB.y, dB.y - dB.min)); + let sc = h / dB.H; + dB.circ.attr("cy", (s) => h * s); + dB.scale.attr("transform", "scale(1," + sc + ")"); + dB.h = 15 * sc; + dB.mid.attrs({ y: dB.y - dB.h, height: 2 * dB.h }); + dB.updatey(); + }), + ); +let yCenter = 60; +dB.updatey = function (_dom) { + let d = (l) => l[1] - l[0]; + y.domain(yR.map((y) => yCenter + ((y - dB.y) * (15 / dB.h) * d(yD)) / d(yR))); + yAxisObj.call(fmtY); + let getTr = (o) => (o ? "translate(0," + (y(o) - y(0)) + ")" : null); + clearLabels(); + gpath.selectAll("path").call(redrawLine); +}; // Label drawing and screenshot -let getFullName = p => p.dispBrand+" "+p.dispName, - getChannelName = p => n => getFullName(p) + " ("+n+")"; +let getFullName = (p) => p.dispBrand + " " + p.dispName, + getChannelName = (p) => (n) => getFullName(p) + " (" + n + ")"; let labelButton = doc.select("#label"), - labelsShown = false; + labelsShown = false; function setLabelButton(l) { - labelButton.classed("selected", labelsShown = l); + labelButton.classed("selected", (labelsShown = l)); } function clearLabels() { - gr.selectAll(".lineLabel").remove(); - setLabelButton(false); + gr.selectAll(".lineLabel").remove(); + setLabelButton(false); } function drawLabels() { - let curves = d3.merge( - activePhones.filter(p=>!p.hide).map(p => - p.isTarget||!p.samp||p.avg ? p.activeCurves - : LR.map((l,i) => ({ - p:p, o:getO(i), id:getChannelName(p)(l), multi:true, - l:(n=>p.channels.slice(i*n,(i+1)*n))(sampnums.length) - .filter(c=>c!==null) - })) - ) - ); - if (!curves.length) return; - - let bcurves = curves.slice(), - bp = baseline.p; - if (bp && bp.hide) { - bcurves.push({ - p:bp, o:0, - id:"Baseline: "+(bp.isTarget?bp.fullName:getFullName(bp)) - }); - } - - gr.selectAll(".lineLabel").remove(); - let g = gr.selectAll(".lineLabel").data(bcurves) - .join("g").attr("class","lineLabel").attr("opacity", 0); - let t = g.append("text") - .attrs({x:0, y:0, fill:c=>getTooltipColor(c)}) - .text(c=>c.id); - g.datum(function(){return this.getBBox();}); - g.select("text").attrs(b=>({x:3-b.x, y:3-b.y})); - g.insert("rect", "text") - .attrs(b=>({x:2, y:2, width:b.width+2, height:b.height+2})); - let boxes = g.data(), - w = boxes.map(b=>b.width +6), - h = boxes.map(b=>b.height+6); - - // Slice to fit in range - let r = x.domain().map(v => d3.bisectLeft(f_values, v)); - rsl = a => a.slice(Math.max(r[0],0), r[1]+1); - let rf_values = rsl(f_values); - let v = curves.map(c => { - let o = getOffset(c.p); - return (c.multi?c.l:[c.l]) - .map(l => rsl(baseline.fn(l).map(d=>d[1]+o))); + let curves = d3.merge( + activePhones + .filter((p) => !p.hide) + .map((p) => + p.isTarget || !p.samp || p.avg + ? p.activeCurves + : LR.map((l, i) => ({ + p: p, + o: getO(i), + id: getChannelName(p)(l), + multi: true, + l: ((n) => p.channels.slice(i * n, (i + 1) * n))(sampnums.length).filter( + (c) => c !== null, + ), + })), + ), + ); + if (!curves.length) return; + + let bcurves = curves.slice(), + bp = baseline.p; + if (bp && bp.hide) { + bcurves.push({ + p: bp, + o: 0, + id: "Baseline: " + (bp.isTarget ? bp.fullName : getFullName(bp)), }); - let tr; - - if (curves.length === 1) { - let x0 = 50, y0 = 10, - sl = range_to_slice([0,w[0]], o=>x0+o), - e = d3.extent(d3.merge(v[0].map(sl)).map(y)); - if (y0+h[0] >= e[0]) { y0 = Math.max(y0, e[1]); } - tr = [[x0,y0]]; - } else { - let n = v.length; - let invd = (sc,d) => sc.invert(d)-sc.invert(0), - xr = x.range(), - yd = y.domain(), - wind = w => Math.ceil((w/(xr[1]-xr[0]))*rf_values.length), - mw = wind(d3.min(w)); - let winReduce = (l,w,d0,fn) => { - l = l.slice(); - for (let d=d0; d getTooltipColor(c) }) + .text((c) => c.id); + g.datum(function () { + return this.getBBox(); + }); + g.select("text").attrs((b) => ({ x: 3 - b.x, y: 3 - b.y })); + g.insert("rect", "text").attrs((b) => ({ x: 2, y: 2, width: b.width + 2, height: b.height + 2 })); + let boxes = g.data(), + w = boxes.map((b) => b.width + 6), + h = boxes.map((b) => b.height + 6); + + // Slice to fit in range + let r = x.domain().map((v) => d3.bisectLeft(f_values, v)); + rsl = (a) => a.slice(Math.max(r[0], 0), r[1] + 1); + let rf_values = rsl(f_values); + let v = curves.map((c) => { + let o = getOffset(c.p); + return (c.multi ? c.l : [c.l]).map((l) => rsl(baseline.fn(l).map((d) => d[1] + o))); + }); + let tr; + + if (curves.length === 1) { + let x0 = 50, + y0 = 10, + sl = range_to_slice([0, w[0]], (o) => x0 + o), + e = d3.extent(d3.merge(v[0].map(sl)).map(y)); + if (y0 + h[0] >= e[0]) { + y0 = Math.max(y0, e[1]); + } + tr = [[x0, y0]]; + } else { + let n = v.length; + let invd = (sc, d) => sc.invert(d) - sc.invert(0), + xr = x.range(), + yd = y.domain(), + wind = (w) => Math.ceil((w / (xr[1] - xr[0])) * rf_values.length), + mw = wind(d3.min(w)); + let winReduce = (l, w, d0, fn) => { + l = l.slice(); + for (let d = d0; d < w; ) { + let diff = Math.min(2 * d, w) - d; + for (let i = 0; i < l.length - diff; i++) { + l[i] = fn(l[i], l[i + diff]); + } + d += diff; + } + l.length -= w - d0; + return l; + }; + let rangeGetters = [Math.min, Math.max].map((f) => { + let r = (c) => c.reduce((a, b) => a.map((ai, i) => f(ai, b[i]))); + let t = v.map((c) => winReduce(r(c), mw, 1, f)); + return (w) => t.map((c) => winReduce(c, w, mw, f)); + }); + let top = 0; // Use top left if we can't find a spot + tr = v.map((_, j) => { + let we = wind(w[j]), + he = -invd(y, h[j]), + range = d3.transpose(rangeGetters.map((r) => r(we))), + ds; + ds = range[j].map(function (r, ri) { + let le = r.length, + s = [ + [-he, 0], + [0, he], + ][ri].map((o) => r.map((d) => d + o)), + d = r.map((_) => 1e10); + for (let k = 0; k < n; k++) + if (k !== j) { + let t = range[k]; + for (let i = 0; i < le; i++) { + d[i] = Math.min(d[i], Math.max(s[0][i] - t[1][i], t[0][i] - s[1][i])); + } + } + return d; + }); + let sep = 0, + pos = null; + ds.forEach(function (drow, k) { + for (let ii = 0; ii < drow.length; ) { + let i = ii, + d = drow[i], + rjk = range[j][k], + m = rjk[i]; + while ((ii++, ii < drow.length && rjk[ii] === m)) { + let di = drow[ii]; + if (di < d && di < 1) break; + d = Math.max(d, drow[ii]); + } + let clip = (x) => x / Math.sqrt(1 + x * x); + d = 4 * clip(d / 4) + clip((ii - i) / 3); + i = Math.floor((i + ii) / 2); + let dl = drow.length, + r = i / dl; + d *= Math.sqrt((0.8 + r) * Math.sqrt(1 - r)); + d *= clip( + 0.2 + Math.max(0, (i >= 15 ? drow[i - 15] : 0) + (i < dl - 15 ? drow[i + 15] : 0)), + ); + if (d > sep) { + let dy = range[j][k][i] + (k ? he : 0), + yd = y.domain(); + if (yd[0] + he <= dy && dy <= yd[1]) { + sep = d; + pos = [i, dy]; } - l.length -= w-d0; - return l; + } } - let rangeGetters = [Math.min, Math.max].map(f => { - let r = c => c.reduce((a,b)=>a.map((ai,i)=>f(ai,b[i]))); - let t = v.map(c => winReduce(r(c), mw, 1, f)); - return w => t.map(c => winReduce(c, w, mw, f)); - }); - let top = 0; // Use top left if we can't find a spot - tr = v.map((_,j) => { - let we = wind(w[j]), - he = -invd(y,h[j]), - range = d3.transpose(rangeGetters.map(r => r(we))), - ds; - ds = range[j].map(function (r,ri) { - let le = r.length, - s = [[-he,0],[0,he]][ri].map(o=>r.map(d=>d+o)), - d = r.map(_=>1e10); - for (let k=0; k x/Math.sqrt(1+x*x); - d = 4*clip(d/4) + clip((ii-i)/3); - i = Math.floor((i+ii)/2); - let dl = drow.length, - r = i/dl; - d *= Math.sqrt((0.8+r)*Math.sqrt(1-r)); - d *= clip(0.2+Math.max(0,(i>=15?drow[i-15]:0)+(isep) { - let dy = range[j][k][i]+(k?he:0), - yd = y.domain(); - if (yd[0]+he<=dy && dy<=yd[1]) { sep=d; pos=[i,dy]; } - } - } - }); - return pos ? [x(rf_values[pos[0]]), y(pos[1])] - : [60, 20+30*top++]; - }); - } - for (let j=curves.length; j"translate("+tr[i].join(",")+")"); - g.attr("opacity",null); - setLabelButton(true); + }); + return pos ? [x(rf_values[pos[0]]), y(pos[1])] : [60, 20 + 30 * top++]; + }); + } + for (let j = curves.length; j < bcurves.length; j++) { + tr.push([pad.l + (W - w[j]) / 2, pad.t + H - h[j] + 2]); + } + g.attr("transform", (_, i) => "translate(" + tr[i].join(",") + ")"); + g.attr("opacity", null); + setLabelButton(true); } -labelButton.on("click", () => (labelsShown?clearLabels:drawLabels)()); +labelButton.on("click", () => (labelsShown ? clearLabels : drawLabels)()); function saveGraph(ext) { - let fn = {png:saveSvgAsPng, svg:saveSvg}[ext]; - let showControls = s => dB.all.attr("visibility",s?null:"hidden"); - gpath.selectAll("path").classed("highlight",false); - drawLabels(); - showControls(false); - fn(gr.node(), "graph."+ext, {scale:3}) - .then(()=>showControls(true)); - - // Analytics event - if (analyticsEnabled) { pushEventTag("clicked_download", targetWindow); } -} -doc.select("#download") - .on("click", () => saveGraph("png")) - .on("contextmenu", function () { - d3.event.returnValue=false; - let b = d3.select(this); - let choice = b.selectAll("div") - .data(["png","svg"]).join("div") - .styles({position:"absolute", left:0, top:(_,i)=>i*1.3+"em", - background:"inherit", padding:"0.1em 1em"}) - .text(d => "As ."+d) - .on("click", function (d) { - saveGraph(d); - choice.remove(); - d3.event.stopPropagation(); - }); - b.on("blur", ()=>choice.remove()); - }); - + let fn = { png: saveSvgAsPng, svg: saveSvg }[ext]; + let showControls = (s) => dB.all.attr("visibility", s ? null : "hidden"); + gpath.selectAll("path").classed("highlight", false); + drawLabels(); + showControls(false); + fn(gr.node(), "graph." + ext, { scale: 3 }).then(() => showControls(true)); + + // Analytics event + if (analyticsEnabled) { + pushEventTag("clicked_download", targetWindow); + } +} +doc + .select("#download") + .on("click", () => saveGraph("png")) + .on("contextmenu", function () { + d3.event.returnValue = false; + let b = d3.select(this); + let choice = b + .selectAll("div") + .data(["png", "svg"]) + .join("div") + .styles({ + position: "absolute", + left: 0, + top: (_, i) => i * 1.3 + "em", + background: "inherit", + padding: "0.1em 1em", + }) + .text((d) => "As ." + d) + .on("click", function (d) { + saveGraph(d); + choice.remove(); + d3.event.stopPropagation(); + }); + b.on("blur", () => choice.remove()); + }); // Graph smoothing -let pair = (arr,fn) => arr.slice(1).map((v,i)=>fn(v,arr[i])); +let pair = (arr, fn) => arr.slice(1).map((v, i) => fn(v, arr[i])); function smooth_prep(h, d) { - let rh = h.map(d=>1/d), - G = [ rh.slice(0,rh.length-1), - pair(rh, (a,b)=>-(a+b)), - rh.slice(1) ], - dv = d3.range(rh.length+1).map(i=>d(i)), - dG = G.map((r,j) => r.map((e,i) => e*dv[i+j])), - d2 = dv.map(e=>e*e), - h6 = h.map(d=>d/6), - M = [ pair(h6, (a,b)=>2*(a+b)), - h6.slice(1,h6.length-1), - h6.slice(3).map(_=>0) ]; - dG.forEach((_,k) => - dG.slice(k).forEach((g,i) => - dG[i].slice(k).forEach((a,j) => M[k][j] += a*g[j]) - ) - ); - - // Diagonal LDL decomposition of M - let md = [M[0][0]], - ml = M.slice(1).map(m=>[m[0]/md]); - d3.range(1,M[0].length).forEach(j => { - let n = ml.length, - p = md.slice(-n).reverse().map((d,i)=>d*ml[i][j-1-i]), - a = M.map((m,k) => m[j] - d3.sum(p.slice(0,n-k), - (a,i) => a*ml[k+i][j-1-i])); - md.push(a[0]); - ml.forEach((l,j)=>l.push(a[j+1]/a[0])); - }); - - return { G:G, md:md, ml:ml, d2:d2 }; + let rh = h.map((d) => 1 / d), + G = [rh.slice(0, rh.length - 1), pair(rh, (a, b) => -(a + b)), rh.slice(1)], + dv = d3.range(rh.length + 1).map((i) => d(i)), + dG = G.map((r, j) => r.map((e, i) => e * dv[i + j])), + d2 = dv.map((e) => e * e), + h6 = h.map((d) => d / 6), + M = [pair(h6, (a, b) => 2 * (a + b)), h6.slice(1, h6.length - 1), h6.slice(3).map((_) => 0)]; + dG.forEach((_, k) => + dG.slice(k).forEach((g, i) => dG[i].slice(k).forEach((a, j) => (M[k][j] += a * g[j]))), + ); + + // Diagonal LDL decomposition of M + let md = [M[0][0]], + ml = M.slice(1).map((m) => [m[0] / md]); + d3.range(1, M[0].length).forEach((j) => { + let n = ml.length, + p = md + .slice(-n) + .reverse() + .map((d, i) => d * ml[i][j - 1 - i]), + a = M.map((m, k) => m[j] - d3.sum(p.slice(0, n - k), (a, i) => a * ml[k + i][j - 1 - i])); + md.push(a[0]); + ml.forEach((l, j) => l.push(a[j + 1] / a[0])); + }); + + return { G: G, md: md, ml: ml, d2: d2 }; } function smooth_eval(p, y) { - let Gy = p.G[0].map(_=>0), - n = Gy.length; - p.G.forEach((r,j) => r.forEach((e,i) => Gy[i] += e*y[i+j])); - // Forward substitution and multiply by p.md - for (let i=0; i { let j=i+k+1; if (j { let j=i-k-1; if (j>=0) Gy[j] -= m[j]*yi; }); - } - let u = y.slice(); - p.G.forEach((r,j) => r.forEach((e,i) => u[i+j] -= e*p.d2[i+j]*Gy[i])); - return u; + let Gy = p.G[0].map((_) => 0), + n = Gy.length; + p.G.forEach((r, j) => r.forEach((e, i) => (Gy[i] += e * y[i + j]))); + // Forward substitution and multiply by p.md + for (let i = 0; i < n; i++) { + let yi = Gy[i]; + p.ml.forEach((m, k) => { + let j = i + k + 1; + if (j < n) Gy[j] -= m[i] * yi; + }); + Gy[i] /= p.md[i]; + } + // Back substitution + for (let i = n; i--; ) { + let yi = Gy[i]; + p.ml.forEach((m, k) => { + let j = i - k - 1; + if (j >= 0) Gy[j] -= m[j] * yi; + }); + } + let u = y.slice(); + p.G.forEach((r, j) => r.forEach((e, i) => (u[i + j] -= e * p.d2[i + j] * Gy[i]))); + return u; } let smooth_level = 5, - smooth_scale = 0.01*(typeof scale_smoothing !== "undefined" ? scale_smoothing : 1), - smooth_param = undefined; + smooth_scale = 0.01 * (typeof scale_smoothing !== "undefined" ? scale_smoothing : 1), + smooth_param = undefined; function smooth(y, c) { - if (smooth_level === 0) { return y; } - let get_param = fv => { - let x = fv.map(f=>Math.log(f)), - h = pair(x, (a,b)=>a-b), - s = smooth_level*smooth_scale, - d = i => s*Math.pow(1/80,Math.pow(i/x.length,2)); - return smooth_prep(h, d); - } - let p; - if (y.length!==f_values.length) { - p = get_param(c.map(d=>d[0])); - } else { - if (!smooth_param) { smooth_param = get_param(f_values); } - p = smooth_param; - } - return smooth_eval(p, y); + if (smooth_level === 0) { + return y; + } + let get_param = (fv) => { + let x = fv.map((f) => Math.log(f)), + h = pair(x, (a, b) => a - b), + s = smooth_level * smooth_scale, + d = (i) => s * Math.pow(1 / 80, Math.pow(i / x.length, 2)); + return smooth_prep(h, d); + }; + let p; + if (y.length !== f_values.length) { + p = get_param(c.map((d) => d[0])); + } else { + if (!smooth_param) { + smooth_param = get_param(f_values); + } + p = smooth_param; + } + return smooth_eval(p, y); } function smoothPhone(p) { - if (p.smooth !== smooth_level) { - p.channels = p.rawChannels.map( - c=>c?smooth(c.map(d=>d[1]),c).map((d,i)=>[c[i][0],d]):c - ); - p.smooth = smooth_level; - setCurves(p); - } + if (p.smooth !== smooth_level) { + p.channels = p.rawChannels.map((c) => + c + ? smooth( + c.map((d) => d[1]), + c, + ).map((d, i) => [c[i][0], d]) + : c, + ); + p.smooth = smooth_level; + setCurves(p); + } } doc.select("#smooth-level").on("change input", function () { - if (!this.checkValidity()) return; - smooth_level = +this.value; - smooth_param = undefined; - line.curve(smooth_level ? d3.curveNatural : d3.curveCardinal.tension(0.5)); - activePhones.forEach(smoothPhone); - updatePaths(); + if (!this.checkValidity()) return; + smooth_level = +this.value; + smooth_param = undefined; + line.curve(smooth_level ? d3.curveNatural : d3.curveCardinal.tension(0.5)); + activePhones.forEach(smoothPhone); + updatePaths(); }); - // Normalization with target loudness -const iso223_params = { // :2003 - f : [ 20, 25, 31.5, 40, 50, 63, 80, 100, 125, 160, 200, 250, 315, 400, 500, 630, 800, 1000, 1250, 1600, 2000, 2500, 3150, 4000, 5000, 6300, 8000, 10000, 12500], - a_f: [0.532, 0.506, 0.48, 0.455, 0.432, 0.409, 0.387, 0.367, 0.349, 0.33, 0.315, 0.301, 0.288, 0.276, 0.267, 0.259, 0.253, 0.25, 0.246, 0.244, 0.243, 0.243, 0.243, 0.242, 0.242, 0.245, 0.254, 0.271, 0.301], - L_U: [-31.6, -27.2, -23, -19.1, -15.9, -13, -10.3, -8.1, -6.2, -4.5, -3.1, -2, -1.1, -0.4, 0, 0.3, 0.5, 0, -2.7, -4.1, -1, 1.7, 2.5, 1.2, -2.1, -7.1, -11.2, -10.7, -3.1], - T_f: [ 78.5, 68.7, 59.5, 51.1, 44, 37.5, 31.5, 26.5, 22.1, 17.9, 14.4, 11.4, 8.6, 6.2, 4.4, 3, 2.2, 2.4, 3.5, 1.7, -1.3, -4.2, -6, -5.4, -1.5, 6, 12.6, 13.9, 12.3] +const iso223_params = { + // :2003 + f: [ + 20, 25, 31.5, 40, 50, 63, 80, 100, 125, 160, 200, 250, 315, 400, 500, 630, 800, 1000, 1250, + 1600, 2000, 2500, 3150, 4000, 5000, 6300, 8000, 10000, 12500, + ], + a_f: [ + 0.532, 0.506, 0.48, 0.455, 0.432, 0.409, 0.387, 0.367, 0.349, 0.33, 0.315, 0.301, 0.288, 0.276, + 0.267, 0.259, 0.253, 0.25, 0.246, 0.244, 0.243, 0.243, 0.243, 0.242, 0.242, 0.245, 0.254, 0.271, + 0.301, + ], + L_U: [ + -31.6, -27.2, -23, -19.1, -15.9, -13, -10.3, -8.1, -6.2, -4.5, -3.1, -2, -1.1, -0.4, 0, 0.3, + 0.5, 0, -2.7, -4.1, -1, 1.7, 2.5, 1.2, -2.1, -7.1, -11.2, -10.7, -3.1, + ], + T_f: [ + 78.5, 68.7, 59.5, 51.1, 44, 37.5, 31.5, 26.5, 22.1, 17.9, 14.4, 11.4, 8.6, 6.2, 4.4, 3, 2.2, + 2.4, 3.5, 1.7, -1.3, -4.2, -6, -5.4, -1.5, 6, 12.6, 13.9, 12.3, + ], }; -const free_field = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.0725,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.0896,0,0,0,0,0,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.0967,0,0,0,0,0,0,0,0.0886,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.0656,0,0,0,0,0,0.024,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.045,0,0,0,0,0,0,0.029,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1524,0.2,0.2,0.2386,0.3395,0.4,0.437,0.5,0.5287,0.6225,0.7,0.7063,0.7962,0.8,0.8941,0.9,0.9863,1,1.0729,1.1,1.1544,1.2,1.2504,1.3,1.3,1.3,1.3,1.3163,1.4,1.4,1.4,1.4,1.4017,1.4846,1.5,1.5,1.5748,1.6,1.6,1.653,1.7,1.7,1.7487,1.8,1.8341,1.9,1.9,1.9229,2,2,2,2.1,2.1,2.1897,2.2,2.2,2.2674,2.3,2.3,2.3567,2.4,2.4,2.4446,2.5,2.5262,2.6,2.6234,2.7149,2.8,2.8038,2.9011,2.9969,3.0913,3.1845,3.2762,3.3757,3.4649,3.5617,3.657,3.751,3.8,3.8432,3.9332,4,4,4,4.0121,4.1,4.1,4.1,4.0079,4,4,4,4,3.9334,3.9,3.9,3.9,3.8541,3.8,3.8,3.768,3.7,3.6761,3.6,3.6,3.5927,3.5,3.5,3.5,3.5,3.5,3.5761,3.6,3.6,3.6604,3.7,3.7514,3.8,3.8,3.8349,3.9,3.9218,4.0199,4.1123,4.2076,4.3016,4.3985,4.6816,5.0515,5.4222,5.8036,6.1097,6.4656,6.8461,7.3316,7.9083,8.4305,8.9369,9.5105,10.0759,10.6024,11.0027,11.4847,12.0482,12.5152,12.8994,13.2776,13.7381,14.1303,14.5168,14.8858,15.273,15.6547,15.9731,16.2596,16.542,16.7857,17.0111,17.2325,17.3532,17.522,17.6,17.6,17.6,17.6,17.5044,17.41,17.3145,17.2205,17.1255,17.0318,16.9373,16.784,16.6459,16.4536,16.2578,16.1234,15.967,15.8736,15.7552,15.566,15.3879,15.2881,15.0958,14.9064,14.8099,14.6287,14.5201,14.3477,14.2307,14.0709,13.9399,13.7916,13.6514,13.5552,13.4604,13.367,13.2718,13.1766,13.0812,12.9743,12.7916,12.6975,12.602,12.5078,12.3247,12.0547,11.7686,11.4154,11.1009,10.9385,10.7344,10.3998,10.0163,9.6382,9.2957,8.9799,8.6248,8.3404,8.0424,7.674,7.3851,7.0061,6.5307,6.1484,5.7696,5.4662,5.1084,4.7302,4.3498,3.971,3.6455,3.4075,3.1343,2.7917,2.5376,2.3484,2.1585,1.9849,1.9107,2,2,2,2.0894,2.1844,2.2787,2.374,2.6057,2.8265,3.0161,3.2057,3.3954,3.5851,3.8122,4.0967,4.354,4.5651,4.8509,5.1459,5.5259,5.9041,6.1881,6.5643,6.8561,7.1418,7.4251,7.7093,8.0593,8.3192,8.4541,8.5493,8.6437,8.7,8.7336,8.8,8.8,8.8,8.8,8.7926,8.7,8.7,8.6079,8.5133,8.5,8.4237,8.1863,7.968,7.7786,7.4219,6.948,6.4299,5.8212,5.1563,4.4634,3.7042,2.8897,1.9005,1.2368,0.5651,-0.2856,-0.8593,-2.9].map(v=>v-7); - -function init_normalize(fv) { // Interpolate values for find_offset - let par = [], ff = []; - par.free_field = ff; - const p = iso223_params; - let i = 0; - fv.forEach(function (f) { - if (f >= p.f[i]) { i++; } - let i0 = Math.max(0,i-1), - i1 = Math.min(i,p.f.length-1), - g; - if (i0===i1) { - g = n => p[n][i0]; - } else { - let ll= [p.f[i0],p.f[i1],f].map(x=>Math.log(x)), - l = (ll[2]-ll[0])/(ll[1]-ll[0]); - g = n => { let v=p[n]; return v[i0]+l*(v[i1]-v[i0]); }; - } - let a = g("a_f"), - m = a * (Math.log10(4)-10 + g("L_U")/10), - k = (0.005076/Math.pow(10,m)) - Math.pow(10, a*g("T_f")/10), - c = Math.pow(10, 9.4 + 4*m) / fv.length; - par.push({a:a, k:k, c:c}); - ffi = Math.floor(0.5+48*Math.log2(f/19.4806)); - ff.push(free_field[Math.max(0,Math.min(479,ffi))]); - }); - return par; +const free_field = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0725, 0.1, 0.1, 0.1, 0.1, 0.1, + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.0896, 0, 0, 0, 0, 0, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, + 0.1, 0.0967, 0, 0, 0, 0, 0, 0, 0, 0.0886, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.0656, 0, 0, 0, 0, + 0, 0.024, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.045, 0, 0, 0, 0, 0, 0, 0.029, 0.1, 0.1, 0.1, + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1524, 0.2, 0.2, 0.2386, 0.3395, 0.4, 0.437, 0.5, + 0.5287, 0.6225, 0.7, 0.7063, 0.7962, 0.8, 0.8941, 0.9, 0.9863, 1, 1.0729, 1.1, 1.1544, 1.2, + 1.2504, 1.3, 1.3, 1.3, 1.3, 1.3163, 1.4, 1.4, 1.4, 1.4, 1.4017, 1.4846, 1.5, 1.5, 1.5748, 1.6, + 1.6, 1.653, 1.7, 1.7, 1.7487, 1.8, 1.8341, 1.9, 1.9, 1.9229, 2, 2, 2, 2.1, 2.1, 2.1897, 2.2, 2.2, + 2.2674, 2.3, 2.3, 2.3567, 2.4, 2.4, 2.4446, 2.5, 2.5262, 2.6, 2.6234, 2.7149, 2.8, 2.8038, 2.9011, + 2.9969, 3.0913, 3.1845, 3.2762, 3.3757, 3.4649, 3.5617, 3.657, 3.751, 3.8, 3.8432, 3.9332, 4, 4, + 4, 4.0121, 4.1, 4.1, 4.1, 4.0079, 4, 4, 4, 4, 3.9334, 3.9, 3.9, 3.9, 3.8541, 3.8, 3.8, 3.768, 3.7, + 3.6761, 3.6, 3.6, 3.5927, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5761, 3.6, 3.6, 3.6604, 3.7, 3.7514, 3.8, + 3.8, 3.8349, 3.9, 3.9218, 4.0199, 4.1123, 4.2076, 4.3016, 4.3985, 4.6816, 5.0515, 5.4222, 5.8036, + 6.1097, 6.4656, 6.8461, 7.3316, 7.9083, 8.4305, 8.9369, 9.5105, 10.0759, 10.6024, 11.0027, + 11.4847, 12.0482, 12.5152, 12.8994, 13.2776, 13.7381, 14.1303, 14.5168, 14.8858, 15.273, 15.6547, + 15.9731, 16.2596, 16.542, 16.7857, 17.0111, 17.2325, 17.3532, 17.522, 17.6, 17.6, 17.6, 17.6, + 17.5044, 17.41, 17.3145, 17.2205, 17.1255, 17.0318, 16.9373, 16.784, 16.6459, 16.4536, 16.2578, + 16.1234, 15.967, 15.8736, 15.7552, 15.566, 15.3879, 15.2881, 15.0958, 14.9064, 14.8099, 14.6287, + 14.5201, 14.3477, 14.2307, 14.0709, 13.9399, 13.7916, 13.6514, 13.5552, 13.4604, 13.367, 13.2718, + 13.1766, 13.0812, 12.9743, 12.7916, 12.6975, 12.602, 12.5078, 12.3247, 12.0547, 11.7686, 11.4154, + 11.1009, 10.9385, 10.7344, 10.3998, 10.0163, 9.6382, 9.2957, 8.9799, 8.6248, 8.3404, 8.0424, + 7.674, 7.3851, 7.0061, 6.5307, 6.1484, 5.7696, 5.4662, 5.1084, 4.7302, 4.3498, 3.971, 3.6455, + 3.4075, 3.1343, 2.7917, 2.5376, 2.3484, 2.1585, 1.9849, 1.9107, 2, 2, 2, 2.0894, 2.1844, 2.2787, + 2.374, 2.6057, 2.8265, 3.0161, 3.2057, 3.3954, 3.5851, 3.8122, 4.0967, 4.354, 4.5651, 4.8509, + 5.1459, 5.5259, 5.9041, 6.1881, 6.5643, 6.8561, 7.1418, 7.4251, 7.7093, 8.0593, 8.3192, 8.4541, + 8.5493, 8.6437, 8.7, 8.7336, 8.8, 8.8, 8.8, 8.8, 8.7926, 8.7, 8.7, 8.6079, 8.5133, 8.5, 8.4237, + 8.1863, 7.968, 7.7786, 7.4219, 6.948, 6.4299, 5.8212, 5.1563, 4.4634, 3.7042, 2.8897, 1.9005, + 1.2368, 0.5651, -0.2856, -0.8593, -2.9, +].map((v) => v - 7); + +function init_normalize(fv) { + // Interpolate values for find_offset + let par = [], + ff = []; + par.free_field = ff; + const p = iso223_params; + let i = 0; + fv.forEach(function (f) { + if (f >= p.f[i]) { + i++; + } + let i0 = Math.max(0, i - 1), + i1 = Math.min(i, p.f.length - 1), + g; + if (i0 === i1) { + g = (n) => p[n][i0]; + } else { + let ll = [p.f[i0], p.f[i1], f].map((x) => Math.log(x)), + l = (ll[2] - ll[0]) / (ll[1] - ll[0]); + g = (n) => { + let v = p[n]; + return v[i0] + l * (v[i1] - v[i0]); + }; + } + let a = g("a_f"), + m = a * (Math.log10(4) - 10 + g("L_U") / 10), + k = 0.005076 / Math.pow(10, m) - Math.pow(10, (a * g("T_f")) / 10), + c = Math.pow(10, 9.4 + 4 * m) / fv.length; + par.push({ a: a, k: k, c: c }); + ffi = Math.floor(0.5 + 48 * Math.log2(f / 19.4806)); + ff.push(free_field[Math.max(0, Math.min(479, ffi))]); + }); + return par; } // Find the appropriate offset (in dB) for fr so that the total loudness // is equal to target (in phon) let norm_par = []; // Cached interpolated ISO parameters function find_offset(c, target) { - let par; - if (c.length!==f_values.length) { - par = init_normalize(c.map(d=>d[0])); - } else { - if (!norm_par.length) { norm_par = init_normalize(f_values); } - par = norm_par; - } - let fr = c.map(v=>v[1]); - let x = 0; // Initial offset - function getStep(o) { - const l10 = Math.log(10)/10; - let v=0, d=0; - par.forEach(function (p,i) { - let a=p.a, k=p.k, c=p.c, ds,v0,v1; - v0 = Math.exp(l10*(fr[i]+o-par.free_field[i])); - ds = l10 * v0; - v1 = k + Math.pow(v0,a); - ds *= a * Math.pow(v0,a-1); - v += c * Math.pow(v1,4); - ds *= c * 4 * Math.pow(v1,3); - d += ds; - }); - // value: Math.log(v)/l10 - // deriv: d / (l10*v) - return (Math.log(v) - target*l10) * (v/d); - } - let dx; - do { - dx = getStep(x); - x -= dx; - } while (Math.abs(dx) > 0.01); - return x; + let par; + if (c.length !== f_values.length) { + par = init_normalize(c.map((d) => d[0])); + } else { + if (!norm_par.length) { + norm_par = init_normalize(f_values); + } + par = norm_par; + } + let fr = c.map((v) => v[1]); + let x = 0; // Initial offset + function getStep(o) { + const l10 = Math.log(10) / 10; + let v = 0, + d = 0; + par.forEach(function (p, i) { + let a = p.a, + k = p.k, + c = p.c, + ds, + v0, + v1; + v0 = Math.exp(l10 * (fr[i] + o - par.free_field[i])); + ds = l10 * v0; + v1 = k + Math.pow(v0, a); + ds *= a * Math.pow(v0, a - 1); + v += c * Math.pow(v1, 4); + ds *= c * 4 * Math.pow(v1, 3); + d += ds; + }); + // value: Math.log(v)/l10 + // deriv: d / (l10*v) + return (Math.log(v) - target * l10) * (v / d); + } + let dx; + do { + dx = getStep(x); + x -= dx; + } while (Math.abs(dx) > 0.01); + return x; } - // File loading and channel management -const LR = typeof default_channels !== "undefined" ? default_channels - : ["L","R"]; -let getO = i => LR.length>1 ? -1+i*2/(LR.length-1) : 0; -const sampnums = typeof num_samples !== "undefined" ? d3.range(1,num_samples+1) - : [""]; +const LR = typeof default_channels !== "undefined" ? default_channels : ["L", "R"]; +let getO = (i) => (LR.length > 1 ? -1 + (i * 2) / (LR.length - 1) : 0); +const sampnums = typeof num_samples !== "undefined" ? d3.range(1, num_samples + 1) : [""]; function loadFiles(p, callback) { - let l = f => d3.text(DIR+f+".txt").catch(()=>null); - let f = p.isTarget ? [l(p.fileName)] - : d3.merge(LR.map(s => - sampnums.map(n => l(p.fileName+" "+s+n)))); - Promise.all(f).then(function (frs) { - if (!frs.some(f=>f!==null)) { - alert("Headphone not found!"); - } else { - let ch = frs.map(f => f && Equalizer.interp(f_values, tsvParse(f))); - ch = ch.filter(c => c !== null); - callback(ch); - } - }); + let l = (f) => d3.text(DIR + f + ".txt").catch(() => null); + let f = p.isTarget + ? [l(p.fileName)] + : d3.merge(LR.map((s) => sampnums.map((n) => l(p.fileName + " " + s + n)))); + Promise.all(f).then(function (frs) { + if (!frs.some((f) => f !== null)) { + alert("Headphone not found!"); + } else { + let ch = frs.map((f) => f && Equalizer.interp(f_values, tsvParse(f))); + ch = ch.filter((c) => c !== null); + callback(ch); + } + }); } -let validChannels = p => p.channels.filter(c=>c!==null); -let numChannels = p => d3.sum(p.channels, c=>c!==null); -let notMultichannel = LR.length===1 ? p=>true : p=>p.isTarget; -let hasChannelSel = p => !notMultichannel(p) && numChannels(p)>1; -let keyExt = LR.length===1 ? 16 : 0; -let keyLeft= keyExt ? 0 : sampnums.length>1 ? 11 : 0; -if (keyLeft) d3.select(".key").style("width","17%") +let validChannels = (p) => p.channels.filter((c) => c !== null); +let numChannels = (p) => d3.sum(p.channels, (c) => c !== null); +let notMultichannel = LR.length === 1 ? (p) => true : (p) => p.isTarget; +let hasChannelSel = (p) => !notMultichannel(p) && numChannels(p) > 1; +let keyExt = LR.length === 1 ? 16 : 0; +let keyLeft = keyExt ? 0 : sampnums.length > 1 ? 11 : 0; +if (keyLeft) d3.select(".key").style("width", "17%"); function avgCurves(curves) { - return curves - .map(c=>c.map(d=>Math.pow(10,d[1]/20))) - .reduce((as,bs) => as.map((a,i) => a+bs[i])) - .map((x,i) => [curves[0][i][0], 20*Math.log10(x/curves.length)]); + return curves + .map((c) => c.map((d) => Math.pow(10, d[1] / 20))) + .reduce((as, bs) => as.map((a, i) => a + bs[i])) + .map((x, i) => [curves[0][i][0], 20 * Math.log10(x / curves.length)]); } function getAvg(p) { - if (p.avg) return p.activeCurves[0].l; - let v = validChannels(p); - return v.length===1 ? v[0] : avgCurves(v); + if (p.avg) return p.activeCurves[0].l; + let v = validChannels(p); + return v.length === 1 ? v[0] : avgCurves(v); } function hasImbalance(p) { - if (!hasChannelSel(p)) return false; - let as = p.channels[0], bs = p.channels[1]; - let s0=0, s1=0; - return as.some((a,i) => { - let d = a[1]-bs[i][1]; - d *= 1/(50 * Math.sqrt(1+Math.pow(a[0]/1e4,6))); - s0 = Math.max(s0+d,0); - s1 = Math.max(s1-d,0); - return Math.max(s0,s1) > max_channel_imbalance; - }); + if (!hasChannelSel(p)) return false; + let as = p.channels[0], + bs = p.channels[1]; + let s0 = 0, + s1 = 0; + return as.some((a, i) => { + let d = a[1] - bs[i][1]; + d *= 1 / (50 * Math.sqrt(1 + Math.pow(a[0] / 1e4, 6))); + s0 = Math.max(s0 + d, 0); + s1 = Math.max(s1 - d, 0); + return Math.max(s0, s1) > max_channel_imbalance; + }); } let activePhones = []; -let baseline0 = { p:null, l:null, fn:l=>l }, - baseline = baseline0; - -let gpath = gr.insert("g",".dBScaler") - .attr("fill","none") - .attr("stroke-width",2.3) - .attr("class", "curves-g") - .attr("mask","url(#graphFade)"); +let baseline0 = { p: null, l: null, fn: (l) => l }, + baseline = baseline0; + +let gpath = gr + .insert("g", ".dBScaler") + .attr("fill", "none") + .attr("stroke-width", 2.3) + .attr("class", "curves-g") + .attr("mask", "url(#graphFade)"); function hl(p, h) { - gpath.selectAll("path").filter(c=>c.p===p).classed("highlight",h); + gpath + .selectAll("path") + .filter((c) => c.p === p) + .classed("highlight", h); } let table = doc.select(".curves"); let ld_p1 = 1.1673039782614187; function getCurveColor(id, o) { - let p1 = ld_p1, - p2 = p1*p1, - p3 = p2*p1; - let t = o/32; - let i=id/p3+0.76, j=id/p2+0.79, k=id/p1+0.32; - if (id < 0) { return d3.hcl(360*(1-(-i)%1),5,66); } // Target - let th = 2*Math.PI*i; - i += Math.cos(th-0.3)/24 + Math.cos(6*th)/32; - let s = Math.sin(2*Math.PI*i); - return d3.hcl(360*((i + t/p2)%1), - 88+30*(j%1 + 1.3*s - t/p3), - 36+22*(k%1 + 1.1*s + 6*t*(1-s))); -} -let getColor_AC = c => getCurveColor(c.p.id, c.o); -let getColor_ph = (p,i) => getCurveColor(p.id, p.activeCurves[i].o); + let p1 = ld_p1, + p2 = p1 * p1, + p3 = p2 * p1; + let t = o / 32; + let i = id / p3 + 0.76, + j = id / p2 + 0.79, + k = id / p1 + 0.32; + if (id < 0) { + return d3.hcl(360 * (1 - (-i % 1)), 5, 66); + } // Target + let th = 2 * Math.PI * i; + i += Math.cos(th - 0.3) / 24 + Math.cos(6 * th) / 32; + let s = Math.sin(2 * Math.PI * i); + return d3.hcl( + 360 * ((i + t / p2) % 1), + 88 + 30 * ((j % 1) + 1.3 * s - t / p3), + 36 + 22 * ((k % 1) + 1.1 * s + 6 * t * (1 - s)), + ); +} +let getColor_AC = (c) => getCurveColor(c.p.id, c.o); +let getColor_ph = (p, i) => getCurveColor(p.id, p.activeCurves[i].o); function getDivColor(id, active) { - let c = getCurveColor(id,0); - c.l = 100-(80-Math.min(c.l,60))/(active?1.5:3); - c.c = (c.c-20)/(active?3:4); - return c; + let c = getCurveColor(id, 0); + c.l = 100 - (80 - Math.min(c.l, 60)) / (active ? 1.5 : 3); + c.c = (c.c - 20) / (active ? 3 : 4); + return c; } function color_curveToText(c) { - if (!alt_layout) { - c.l = c.l/5 + 10; - c.c /= 3; - } - return c; -} -let getTooltipColor = curve => color_curveToText(getColor_AC(curve)); -let getTextColor = p => color_curveToText(getCurveColor(p.id,0)); -let getBgColor = p => { - let c=getCurveColor(p.id,0).rgb(); - ['r','g','b'].forEach(p=>c[p]=255-(255-Math.max(0,c[p]))*0.85); - return c; -} + if (!alt_layout) { + c.l = c.l / 5 + 10; + c.c /= 3; + } + return c; +} +let getTooltipColor = (curve) => color_curveToText(getColor_AC(curve)); +let getTextColor = (p) => color_curveToText(getCurveColor(p.id, 0)); +let getBgColor = (p) => { + let c = getCurveColor(p.id, 0).rgb(); + ["r", "g", "b"].forEach((p) => (c[p] = 255 - (255 - Math.max(0, c[p])) * 0.85)); + return c; +}; let cantCompare; let noTargets = typeof disallow_target !== "undefined" && disallow_target; if (noTargets || typeof max_compare !== "undefined") { - const currency = [ - ["$", "#348542"], - ["¥", "#d11111"], - ["€", "#2961d4"], - ["฿", "#dcaf1d"] - ]; - let currencyCounter = -1, - lastMessage = null, - messageWeight = 0; - let cantTarget = p => false; - if (noTargets) { - if (typeof allow_targets === "undefined") { - cantTarget = p => p.isTarget; - } else { - let r = f => f.replace(/ Target$/,""), - a = allow_targets.map(r); - cantTarget = p => p.isTarget && a.indexOf(r(p.fileName))<0; - } - } - let ct = typeof restrict_target === "undefined" || restrict_target, - ccfilter = ct ? (l => l) : (l => l.filter(p=>!p.isTarget)); - cantCompare = function(ps, add, p, noMessage) { - let count = ccfilter(ps).length + (add||0) - (!ct&&p&&p.isTarget?1:0); - if (count=2) { - messageWeight /= 2; - let button = div.attr("class","cashMessage") - .html(premium_html) - .append("button").text("Fine") - .on("mousedown", ()=>messageWeight=0); - button.node().focus(); - let back = doc.append("div") - .attr("class","fadeAll"); - [button,back].forEach(e => - e.on("click", ()=>[div,back].forEach(e=>e.remove())) - ); - } else { - div.attr("class","cash") - .style("color",c[1]).text(c[0]) - .transition().duration(120).remove(); - } - return true; + const currency = [ + ["$", "#348542"], + ["¥", "#d11111"], + ["€", "#2961d4"], + ["฿", "#dcaf1d"], + ]; + let currencyCounter = -1, + lastMessage = null, + messageWeight = 0; + let cantTarget = (p) => false; + if (noTargets) { + if (typeof allow_targets === "undefined") { + cantTarget = (p) => p.isTarget; + } else { + let r = (f) => f.replace(/ Target$/, ""), + a = allow_targets.map(r); + cantTarget = (p) => p.isTarget && a.indexOf(r(p.fileName)) < 0; + } + } + let ct = typeof restrict_target === "undefined" || restrict_target, + ccfilter = ct ? (l) => l : (l) => l.filter((p) => !p.isTarget); + cantCompare = function (ps, add, p, noMessage) { + let count = ccfilter(ps).length + (add || 0) - (!ct && p && p.isTarget ? 1 : 0); + if (count < max_compare && !(p && cantTarget(p))) { + return false; + } + if (noMessage) { + return true; + } + let div = doc.append("div"); + let c = currency[currencyCounter++ % currency.length]; + let lm = lastMessage; + lastMessage = Date.now(); + messageWeight *= Math.pow(2, (lm ? lm - lastMessage : 0) / 3e4); // 30-second half-life + messageWeight++; + if (!currencyCounter || messageWeight >= 2) { + messageWeight /= 2; + let button = div + .attr("class", "cashMessage") + .html(premium_html) + .append("button") + .text("Fine") + .on("mousedown", () => (messageWeight = 0)); + button.node().focus(); + let back = doc.append("div").attr("class", "fadeAll"); + [button, back].forEach((e) => e.on("click", () => [div, back].forEach((e) => e.remove()))); + } else { + div.attr("class", "cash").style("color", c[1]).text(c[0]).transition().duration(120).remove(); } + return true; + }; } else { - cantCompare = function(m) { return false; } + cantCompare = function (m) { + return false; + }; } let phoneNumber = 0; // I'm so sorry it just happened // Find a phone id which doesn't have a color conflict with pins let nextPN = 0; // Cached value; invalidated when pinned headphones change function nextPhoneNumber() { - if (nextPN === null) { - nextPN = phoneNumber; - let pin = activePhones.filter(p => p.pin).map(p=>p.id); - if (pin.length) { - let p3 = ld_p1*ld_p1*ld_p1, - l = a => b => Math.abs(((a-b)/p3 + 0.5) % 1 - 0.5), - d = id => d3.min(pin, l(id)); - for (let i=nextPN, max=d(i); max<0.12 && ++i max) { max=m; nextPN=i; } - } + if (nextPN === null) { + nextPN = phoneNumber; + let pin = activePhones.filter((p) => p.pin).map((p) => p.id); + if (pin.length) { + let p3 = ld_p1 * ld_p1 * ld_p1, + l = (a) => (b) => Math.abs((((a - b) / p3 + 0.5) % 1) - 0.5), + d = (id) => d3.min(pin, l(id)); + for (let i = nextPN, max = d(i); max < 0.12 && ++i < phoneNumber + 3; ) { + let m = d(i); + if (m > max) { + max = m; + nextPN = i; } + } } - return nextPN; + } + return nextPN; } function getPhoneNumber() { - let pn = nextPhoneNumber(); - phoneNumber = pn + 1; - nextPN = null; - return pn; + let pn = nextPhoneNumber(); + phoneNumber = pn + 1; + nextPN = null; + return pn; } function setPhoneTr(phtr) { - phtr.each(function (p) { - p.highlight = p.active; - let o = p.objs; if (!o) return; - p.objs = o = o.filter(q=>q.active); - if (o.length === 0) { - delete p.objs; - } else if (!p.active) { - p.id = o[0].id; - p.highlight = true; - } + phtr.each(function (p) { + p.highlight = p.active; + let o = p.objs; + if (!o) return; + p.objs = o = o.filter((q) => q.active); + if (o.length === 0) { + delete p.objs; + } else if (!p.active) { + p.id = o[0].id; + p.highlight = true; + } + }); + phtr + .style("background", (p) => (p.isTarget && !p.active ? null : getDivColor(p.id, p.highlight))) + .style("border-color", (p) => (p.highlight ? getDivColor(p.id, 1) : null)); + phtr + .filter((p) => !p.isTarget) + .select(".phone-item-add") + .selectAll(".remove") + .data((p) => (p.highlight ? [p] : [])) + .join("span") + .attr("class", "remove") + .text("⊗") + .on("click", (p) => { + d3.event.stopPropagation(); + removeCopies(p); }); - phtr.style("background",p=>p.isTarget&&!p.active?null:getDivColor(p.id,p.highlight)) - .style("border-color",p=>p.highlight?getDivColor(p.id,1):null); - phtr.filter(p=>!p.isTarget) - .select(".phone-item-add") - .selectAll(".remove").data(p=>p.highlight?[p]:[]) - .join("span").attr("class","remove").text("⊗") - .on("click", p => { d3.event.stopPropagation(); removeCopies(p); }); } -let channelbox_x = c => c?-86:-36, - channelbox_tr = c => "translate("+channelbox_x(c)+",0)"; +let channelbox_x = (c) => (c ? -86 : -36), + channelbox_tr = (c) => "translate(" + channelbox_x(c) + ",0)"; function setCurves(p, avg, lr, samp) { - if (avg ===undefined) avg = p.avg; - if (samp===undefined) samp = avg ? false : LR.length===1||p.ssamp||false; - else { p.ssamp = samp; if (samp) avg = false; } - let dx = +avg - +p.avg, - n = p.channels.length/2, - selCh = (l,i) => l.slice(i*n,(i+1)*n); - p.avg = avg; - p.samp = samp = n>1 && samp; - if (!p.isTarget) { - let id = getChannelName(p), - v = cs => cs.filter(c=>c!==null), - cs = p.channels, - cv = v(cs), - mc = cv.length>1, - pc = (idstr, l, oi) => ({id:id(idstr), l:l, p:p, - o:oi===undefined?0:getO(oi)}); - p.activeCurves - = avg && mc ? [pc("AVG", avgCurves(cv))] - : !samp && mc ? LR.map((l,i) => pc(l, avgCurves(v(selCh(cs,i))), i)) - : cs.map((l,i) => { - let j = Math.floor(i/n); - return pc(LR[j]+sampnums[i%n], l, j); - }).filter(c => c.l); - } else { - p.activeCurves = [{id:p.fullName, l:p.channels[0], p:p, o:0}]; - } - let y = 0; - let k = d3.selectAll(".keyLine").filter(q=>q===p); - let ksb = k.select(".keySelBoth").attr("display","none"); - p.lr = lr; - if (lr!==undefined) { - p.activeCurves = p.samp ? selCh(p.activeCurves, lr) : [p.activeCurves[lr]]; - y = [-1,1][lr]; - ksb.attr("display",null).attr("y", [0,-12][lr]); - } - k.select(".keyMask") - .transition().duration(400) - .attr("x", channelbox_x(avg)) - .attrTween("y", function () { - let y0 = +this.getAttribute("y"), - y1 = 12*(-1+y); - if (!dx) { return d3.interpolateNumber(y0,y1); } - let ym = y0 + (y1-y0)*(3-2*dx)/6; - y0-=ym; y1-=ym; - return t => { t-=1/2; return ym+(t<0?y0:y1)*Math.pow(2,20*(Math.abs(t)-1/2)); }; - }); - k.select(".keySel").attr("transform", channelbox_tr(avg)); - k.selectAll(".keySamp").attr("opacity",(_,i)=>i===+samp?1:0.6); + if (avg === undefined) avg = p.avg; + if (samp === undefined) samp = avg ? false : LR.length === 1 || p.ssamp || false; + else { + p.ssamp = samp; + if (samp) avg = false; + } + let dx = +avg - +p.avg, + n = p.channels.length / 2, + selCh = (l, i) => l.slice(i * n, (i + 1) * n); + p.avg = avg; + p.samp = samp = n > 1 && samp; + if (!p.isTarget) { + let id = getChannelName(p), + v = (cs) => cs.filter((c) => c !== null), + cs = p.channels, + cv = v(cs), + mc = cv.length > 1, + pc = (idstr, l, oi) => ({ id: id(idstr), l: l, p: p, o: oi === undefined ? 0 : getO(oi) }); + p.activeCurves = + avg && mc + ? [pc("AVG", avgCurves(cv))] + : !samp && mc + ? LR.map((l, i) => pc(l, avgCurves(v(selCh(cs, i))), i)) + : cs + .map((l, i) => { + let j = Math.floor(i / n); + return pc(LR[j] + sampnums[i % n], l, j); + }) + .filter((c) => c.l); + } else { + p.activeCurves = [{ id: p.fullName, l: p.channels[0], p: p, o: 0 }]; + } + let y = 0; + let k = d3.selectAll(".keyLine").filter((q) => q === p); + let ksb = k.select(".keySelBoth").attr("display", "none"); + p.lr = lr; + if (lr !== undefined) { + p.activeCurves = p.samp ? selCh(p.activeCurves, lr) : [p.activeCurves[lr]]; + y = [-1, 1][lr]; + ksb.attr("display", null).attr("y", [0, -12][lr]); + } + k.select(".keyMask") + .transition() + .duration(400) + .attr("x", channelbox_x(avg)) + .attrTween("y", function () { + let y0 = +this.getAttribute("y"), + y1 = 12 * (-1 + y); + if (!dx) { + return d3.interpolateNumber(y0, y1); + } + let ym = y0 + ((y1 - y0) * (3 - 2 * dx)) / 6; + y0 -= ym; + y1 -= ym; + return (t) => { + t -= 1 / 2; + return ym + (t < 0 ? y0 : y1) * Math.pow(2, 20 * (Math.abs(t) - 1 / 2)); + }; + }); + k.select(".keySel").attr("transform", channelbox_tr(avg)); + k.selectAll(".keySamp").attr("opacity", (_, i) => (i === +samp ? 1 : 0.6)); } function updateCurves() { - setCurves.apply(null, arguments); - updatePaths(); + setCurves.apply(null, arguments); + updatePaths(); } -let drawLine = d => line(baseline.fn(d.l)); +let drawLine = (d) => line(baseline.fn(d.l)); function redrawLine(p) { - let getTr = o => o ? "translate(0,"+(y(o)-y(0))+")" : null; - p.attr("transform", c => getTr(getOffset(c.p))).attr("d", drawLine); + let getTr = (o) => (o ? "translate(0," + (y(o) - y(0)) + ")" : null); + p.attr("transform", (c) => getTr(getOffset(c.p))).attr("d", drawLine); } function updateYCenter() { - let c = yCenter; - yCenter = baseline.p ? 0 : norm_sel ? 60 : norm_phon; - y.domain(y.domain().map(d=>d+(yCenter-c))); - yAxisObj.call(fmtY); + let c = yCenter; + yCenter = baseline.p ? 0 : norm_sel ? 60 : norm_phon; + y.domain(y.domain().map((d) => d + (yCenter - c))); + yAxisObj.call(fmtY); } function setBaseline(b, no_transition) { - baseline = b; - updateYCenter(); - if (no_transition) return; - gpath.selectAll("path") - .transition().duration(500).ease(d3.easeQuad) - .attr("d", drawLine); - table.selectAll("tr").select(".button-baseline") - .classed("selected", p=>p===baseline.p); - - // Analytics event - if (analyticsEnabled && b.p) { pushPhoneTag("baseline_set", b.p); } + baseline = b; + updateYCenter(); + if (no_transition) return; + gpath.selectAll("path").transition().duration(500).ease(d3.easeQuad).attr("d", drawLine); + table + .selectAll("tr") + .select(".button-baseline") + .classed("selected", (p) => p === baseline.p); + + // Analytics event + if (analyticsEnabled && b.p) { + pushPhoneTag("baseline_set", b.p); + } } function getBaseline(p) { - let b = getAvg(p).map(d => d[1]+getOffset(p)); - return { p:p, fn:l=>l.map((e,i)=>[e[0],e[1]-b[Math.min(i,b.length-1)]]) }; + let b = getAvg(p).map((d) => d[1] + getOffset(p)); + return { p: p, fn: (l) => l.map((e, i) => [e[0], e[1] - b[Math.min(i, b.length - 1)]]) }; } function setOffset(p, o) { - p.offset = +o; - if (baseline.p === p) { baseline = getBaseline(p); } - updatePaths(); + p.offset = +o; + if (baseline.p === p) { + baseline = getBaseline(p); + } + updatePaths(); } -let getOffset = p => p.offset + p.norm; +let getOffset = (p) => p.offset + p.norm; function setHover(elt, h) { - elt.on("mouseover", h(true)).on("mouseout", h(false)); + elt.on("mouseover", h(true)).on("mouseout", h(false)); } // See if iframe gets CORS error when interacting with window.top try { - let emb = window.location.href.includes('embed'); - - accessWindowTop = (window.top.location.href) ? true:false; - targetWindow = emb ? window : window.top; + let emb = window.location.href.includes("embed"); + + accessWindowTop = window.top.location.href ? true : false; + targetWindow = emb ? window : window.top; } catch { - accessWindowTop = false; - targetWindow = window; + accessWindowTop = false; + targetWindow = window; } // See if iframe gets CORS error when interacting with window.top.document try { - accessDocumentTop = (window.top.document) ? true:false; + accessDocumentTop = window.top.document ? true : false; } catch { - accessDocumentTop = false; + accessDocumentTop = false; } let ifURL = typeof share_url !== "undefined" && share_url; let baseTitle = typeof page_title !== "undefined" ? page_title : "CrinGraph"; -let baseDescription = typeof page_description !== "undefined" ? page_description : "View and compare frequency response graphs"; -let baseURL; // Set by setInitPhones +let baseDescription = + typeof page_description !== "undefined" + ? page_description + : "View and compare frequency response graphs"; +let baseURL; // Set by setInitPhones function addPhonesToUrl() { - let title = baseTitle, - url = baseURL, - names = activePhones.filter(p => !p.isDynamic).map(p => p.fileName), - namesCombined = names.join(", "); - - if (names.length) { - url += "?share=" + encodeURI(names.join().replace(/ /g,"_")); - title = namesCombined + " - " + title; - } - if (names.length === 1) { - targetWindow.document.querySelector("link[rel='canonical']").setAttribute("href",url) - } else { - targetWindow.document.querySelector("link[rel='canonical']").setAttribute("href",baseURL) - } - targetWindow.history.replaceState("", title, url); - targetWindow.document.title = title; - targetWindow.document.querySelector("meta[name='description']").setAttribute("content",baseDescription + ", including " + namesCombined +"."); + let title = baseTitle, + url = baseURL, + names = activePhones.filter((p) => !p.isDynamic).map((p) => p.fileName), + namesCombined = names.join(", "); + + if (names.length) { + url += "?share=" + encodeURI(names.join().replace(/ /g, "_")); + title = namesCombined + " - " + title; + } + if (names.length === 1) { + targetWindow.document.querySelector("link[rel='canonical']").setAttribute("href", url); + } else { + targetWindow.document.querySelector("link[rel='canonical']").setAttribute("href", baseURL); + } + targetWindow.history.replaceState("", title, url); + targetWindow.document.title = title; + targetWindow.document + .querySelector("meta[name='description']") + .setAttribute("content", baseDescription + ", including " + namesCombined + "."); } function setModeEmbed() { - document.querySelector("body").setAttribute("embed-mode", "true"); + document.querySelector("body").setAttribute("embed-mode", "true"); } function updatePaths(trigger) { - clearLabels(); - let c = d3.merge(activePhones.map(p => p.activeCurves)), - p = gpath.selectAll("path").data(c, d=>d.id); - let t = p.join("path").attr("opacity", c=>c.p.hide?0:null) - .classed("sample", c=>c.p.samp) - .attr("stroke", getColor_AC).call(redrawLine) - .filter(c=>c.p.isTarget) - .attr("data-phone-name", c=>c.p.fullName) - .attr("class", "target"); - if (targetDashed) t.style("stroke-dasharray", "6, 3"); - if (targetColorCustom) t.attr("stroke", targetColorCustom); - if (ifURL && !trigger) addPhonesToUrl(); - if (stickyLabels) drawLabels(); -} -let colorBar = p=>'url(\'data:image/svg+xml,\')'; + clearLabels(); + let c = d3.merge(activePhones.map((p) => p.activeCurves)), + p = gpath.selectAll("path").data(c, (d) => d.id); + let t = p + .join("path") + .attr("opacity", (c) => (c.p.hide ? 0 : null)) + .classed("sample", (c) => c.p.samp) + .attr("stroke", getColor_AC) + .call(redrawLine) + .filter((c) => c.p.isTarget) + .attr("data-phone-name", (c) => c.p.fullName) + .attr("class", "target"); + if (targetDashed) t.style("stroke-dasharray", "6, 3"); + if (targetColorCustom) t.attr("stroke", targetColorCustom); + if (ifURL && !trigger) addPhonesToUrl(); + if (stickyLabels) drawLabels(); +} +let colorBar = (p) => + 'url(\'data:image/svg+xml,')"; function updatePhoneTable(trigger) { - let c = table.selectAll("tr").data(activePhones, p=>p.fileName); - c.exit().remove(); - - let f = c.enter().append("tr").attr("data-filename", p=>p.fileName), - td = () => f.append("td"); - f .call(setHover, h => p => hl(p,h)) - .style("color", p => getDivColor(p.id,true)); - - td().attr("class","remove").text("⊗") - .attr("title", "Remove graph") - .on("click", removePhone) - .style("background-image",colorBar) - .filter(p=>!p.isTarget).append("svg").call(addColorPicker); - td().attr("class","item-line item-target") - .call(s=>s.filter(p=>!p.isTarget).attr("class","item-line item-phone") - .append("span").attr("class","brand").text(p=>p.dispBrand)) - .call(addModel); - td().attr("class","curve-color").append("button") - .style("background-color",p=>getCurveColor(p.id,0)) - .filter(p=>!p.isTarget).call(makeColorPicker); - td().attr("class","channels").append("svg").call(addKey) - td().attr("class","levels").append("input") - .attrs({type:"number",step:"any",value:0}) - .property("value", p=>p.offset) - .on("change input",function(p){ setOffset(p, +this.value); }); - if (exportableGraphs) { - td().attr("class","button button-export") - .attr("title", "Export graph") - .on("click", function(p) { - let phoneName = p.fullName, - channels = p.rawChannels, - exportContainer = document.querySelector('body'); - - channels.forEach(function(channel, i) { - let channelNum = i + 1, - text = channel.reduce((acc, c) => { - return acc.concat([Object.values(c).join('\t')]); - }, []).join('\n'), - blob = new Blob([text], { type: 'text/plain' }), - url = URL.createObjectURL(blob), - exportLink = document.createElement('a'); - - exportLink.download = phoneName + ' [' + channelNum + ']' + '.txt'; - exportLink.href = url; - exportContainer.appendChild(exportLink); - exportLink.click(); - exportLink.remove(); - }); + let c = table.selectAll("tr").data(activePhones, (p) => p.fileName); + c.exit().remove(); + + let f = c + .enter() + .append("tr") + .attr("data-filename", (p) => p.fileName), + td = () => f.append("td"); + f.call(setHover, (h) => (p) => hl(p, h)).style("color", (p) => getDivColor(p.id, true)); + + td() + .attr("class", "remove") + .text("⊗") + .attr("title", "Remove graph") + .on("click", removePhone) + .style("background-image", colorBar) + .filter((p) => !p.isTarget) + .append("svg") + .call(addColorPicker); + td() + .attr("class", "item-line item-target") + .call((s) => + s + .filter((p) => !p.isTarget) + .attr("class", "item-line item-phone") + .append("span") + .attr("class", "brand") + .text((p) => p.dispBrand), + ) + .call(addModel); + td() + .attr("class", "curve-color") + .append("button") + .style("background-color", (p) => getCurveColor(p.id, 0)) + .filter((p) => !p.isTarget) + .call(makeColorPicker); + td().attr("class", "channels").append("svg").call(addKey); + td() + .attr("class", "levels") + .append("input") + .attrs({ type: "number", step: "any", value: 0 }) + .property("value", (p) => p.offset) + .on("change input", function (p) { + setOffset(p, +this.value); + }); + if (exportableGraphs) { + td() + .attr("class", "button button-export") + .attr("title", "Export graph") + .on("click", function (p) { + let phoneName = p.fullName, + channels = p.rawChannels, + exportContainer = document.querySelector("body"); + + channels.forEach(function (channel, i) { + let channelNum = i + 1, + text = channel + .reduce((acc, c) => { + return acc.concat([Object.values(c).join("\t")]); + }, []) + .join("\n"), + blob = new Blob([text], { type: "text/plain" }), + url = URL.createObjectURL(blob), + exportLink = document.createElement("a"); + + exportLink.download = phoneName + " [" + channelNum + "]" + ".txt"; + exportLink.href = url; + exportContainer.appendChild(exportLink); + exportLink.click(); + exportLink.remove(); }); - } - td().attr("class","button button-baseline") - .attr("title", "Set as baseline") - .html("") - .on("click", p => setBaseline(p===baseline.p ? baseline0 - : getBaseline(p))); - function toggleHide(p) { - let h = p.hide; - let t = table.selectAll("tr").filter(q=>q===p); - t.select(".keyLine").on("click", h?null:toggleHide) - .selectAll("path,.imbalance").attr("opacity", h?null:0.5); - t.select(".hideIcon").classed("selected", !h); - gpath.selectAll("path").filter(c=>c.p===p) - .attr("opacity", h?null:0); - p.hide = !h; - if (labelsShown) { - clearLabels(); - drawLabels(); - } - } - td().attr("class","button hideIcon") - .attr("title", "Hide graph") - .html("") - .on("click", toggleHide); - td().attr("class","button button-pin") - .attr("title", "Pin graph") - .attr("data-pinned","false") - .html("") - .on("click",function(p){ - if (cantCompare(activePhones.filter(p=>p.pin),1)) return; - - if ( p.pin ) { - p.pin = false; - this.setAttribute("data-pinned","false"); - } else { - p.pin = true; nextPN = null; - this.setAttribute("data-pinned","true"); - } + }); + } + td() + .attr("class", "button button-baseline") + .attr("title", "Set as baseline") + .html("") + .on("click", (p) => setBaseline(p === baseline.p ? baseline0 : getBaseline(p))); + function toggleHide(p) { + let h = p.hide; + let t = table.selectAll("tr").filter((q) => q === p); + t.select(".keyLine") + .on("click", h ? null : toggleHide) + .selectAll("path,.imbalance") + .attr("opacity", h ? null : 0.5); + t.select(".hideIcon").classed("selected", !h); + gpath + .selectAll("path") + .filter((c) => c.p === p) + .attr("opacity", h ? null : 0); + p.hide = !h; + if (labelsShown) { + clearLabels(); + drawLabels(); + } + } + td() + .attr("class", "button hideIcon") + .attr("title", "Hide graph") + .html("") + .on("click", toggleHide); + td() + .attr("class", "button button-pin") + .attr("title", "Pin graph") + .attr("data-pinned", "false") + .html("") + .on("click", function (p) { + if ( + cantCompare( + activePhones.filter((p) => p.pin), + 1, + ) + ) + return; - p.pin = true; nextPN = null; - d3.select(this) - .text(null).classed("button",false).on("click",null) - .insert("svg").attr("class","pinMark") - .attr("viewBox","0 0 280 145") - .insert("path").attrs({ - fill:"none", - "stroke-width":30, - "stroke-linecap":"round", - d:"M265 110V25q0 -10 -10 -10H105q-24 0 -48 20l-24 20q-24 20 -2 40l18 15q24 20 42 20h100" - }); - if (!userConfigApplicationActive) setUserConfig(); + if (p.pin) { + p.pin = false; + this.setAttribute("data-pinned", "false"); + } else { + p.pin = true; + nextPN = null; + this.setAttribute("data-pinned", "true"); + } + + p.pin = true; + nextPN = null; + d3.select(this) + .text(null) + .classed("button", false) + .on("click", null) + .insert("svg") + .attr("class", "pinMark") + .attr("viewBox", "0 0 280 145") + .insert("path") + .attrs({ + fill: "none", + "stroke-width": 30, + "stroke-linecap": "round", + d: "M265 110V25q0 -10 -10 -10H105q-24 0 -48 20l-24 20q-24 20 -2 40l18 15q24 20 42 20h100", }); + if (!userConfigApplicationActive) setUserConfig(); + }); } function addKey(s) { - let dim={x:-19-keyLeft, y:-12, width:65+keyLeft, height:24} - s.attr("class","keyLine").attr("viewBox",[dim.x,dim.y,dim.width,dim.height].join(" ")); - let defs = s.append("defs"); - defs.append("linearGradient").attr("id", p=>"chgrad"+p.id) - .attrs({x1:0,y1:0, x2:0,y2:1}) - .selectAll().data(p=>[0.1,0.4,0.6,0.9].map(o => - [o, getCurveColor(p.id, o<0.3?-1:o<0.7?0:1)] - )).join("stop") - .attr("offset",i=>i[0]) - .attr("stop-color",i=>i[1]); - defs.append("linearGradient").attr("id","blgrad") - .selectAll().data([0,0.25,0.31,0.69,0.75,1]).join("stop") - .attr("offset",o=>o) - .attr("stop-color",(o,i) => i==2||i==3?"white":"#333"); - let m = defs.append("mask").attr("id",p=>"chmask"+p.id); - m.append("rect").attrs(dim).attr("fill","#333"); - m.append("rect").attrs({"class":"keyMask", x:p=>channelbox_x(p.avg), y:-12, width:120, height:24, fill:"url(#blgrad)"}); - let t = s.append("g"); - t.append("path") - .attr("stroke", p => notMultichannel(p) ? getCurveColor(p.id,0) - : "url(#chgrad"+p.id+")"); - t.selectAll().data(p=>p.isTarget?[]:LR) - .join("text").attr("class","keyCLabel") - .attrs({x:17+keyExt, y:(_,i)=>12*(i-(LR.length-1)/2), - dy:"0.32em", "text-anchor":"start", "font-size":10.5}) - .text(t=>t); - t.filter(p=>p.isTarget).append("text") - .attrs(keyExt?{x:7,y:6,"text-anchor":"middle"} - :{x:17,y:0,"text-anchor":"start"}) - .attrs({dy:"0.32em", "font-size":8, fill:p=>getCurveColor(p.id,0)}) - .text("Target"); - let uchl = f => function (p) { - updateCurves(p, f(p)); hl(p,true); - } - s.append("rect").attr("class","keySelBoth") - .attrs({x:40+channelbox_x(0), width:40, height:12, - opacity:0, display:"none"}) - .on("click", uchl(p=>0)); - s.append("g").attr("class","keySel") - .attr("transform",p=>channelbox_tr(p.avg)) - .on("click", uchl(p=>!p.avg)) - .selectAll().data([0,80]).join("rect") - .attrs({x:d=>d, y:-12, width:40, height:24, opacity:0}); - let o = s.filter(p=>!notMultichannel(p)) - .selectAll().data(p=>[[p,0],[p,1]]) - .join("g").attr("class","keyOnly") - .attr("transform",pi=>"translate(25,"+[-6,6][pi[1]]+")") - .call(setHover, h => function (pi) { - let p = pi[0], cs = p.activeCurves; - if (!p.hide && cs.length===2) { - d3.event.stopPropagation(); - hl(p, h ? (c=>c===cs[pi[1]]) : true); - clearLabels(); - gpath.selectAll("path").filter(c=>c.p===p).attr("opacity",h ? (c=>c!==cs[pi[1]]?0.7:null) : null); - } - }) - .on("click", pi => updateCurves(pi[0], false, pi[1])); - o.append("rect").attrs({x:0,y:-6,width:30,height:12,opacity:0}); - o.append("text").attrs({x:0, y:0, dy:"0.28em", "text-anchor":"start", - "font-size":7.5 }) - .text("only"); - s.append("text").attr("class","imbalance") - .attrs({x:8,y:0,dy:"0.35em","font-size":10.5}) - .text("!"); - if (sampnums.length>1) { - let a = s.filter(p=>!p.isTarget); - let f = LR.length>1 ? (n=>"all "+n) : (n=>n+" samples"); - let t = a.selectAll() - .data(p=>["AVG",f(Math.floor(validChannels(p).length/LR.length))] - .map((t,i)=>[t,i===+p.samp?1:0.6])) - .join("text").attr("class","keySamp") - .attrs({x:-18.5-keyLeft, y:(_,i)=>12*(i-1/2), dy:"0.33em", - "text-anchor":"start", "font-size":7, opacity:t=>t[1] }) - .text(t=>t[0]); - a.append("rect") - .attrs({x:-19-keyLeft, y:-12, width:keyLeft?16:38, height:24, opacity:0}) - .on("click", p=>updateCurves(p, undefined, p.lr, !p.samp)); - } - updateKey(s); + let dim = { x: -19 - keyLeft, y: -12, width: 65 + keyLeft, height: 24 }; + s.attr("class", "keyLine").attr("viewBox", [dim.x, dim.y, dim.width, dim.height].join(" ")); + let defs = s.append("defs"); + defs + .append("linearGradient") + .attr("id", (p) => "chgrad" + p.id) + .attrs({ x1: 0, y1: 0, x2: 0, y2: 1 }) + .selectAll() + .data((p) => + [0.1, 0.4, 0.6, 0.9].map((o) => [o, getCurveColor(p.id, o < 0.3 ? -1 : o < 0.7 ? 0 : 1)]), + ) + .join("stop") + .attr("offset", (i) => i[0]) + .attr("stop-color", (i) => i[1]); + defs + .append("linearGradient") + .attr("id", "blgrad") + .selectAll() + .data([0, 0.25, 0.31, 0.69, 0.75, 1]) + .join("stop") + .attr("offset", (o) => o) + .attr("stop-color", (o, i) => (i == 2 || i == 3 ? "white" : "#333")); + let m = defs.append("mask").attr("id", (p) => "chmask" + p.id); + m.append("rect").attrs(dim).attr("fill", "#333"); + m.append("rect").attrs({ + class: "keyMask", + x: (p) => channelbox_x(p.avg), + y: -12, + width: 120, + height: 24, + fill: "url(#blgrad)", + }); + let t = s.append("g"); + t.append("path").attr("stroke", (p) => + notMultichannel(p) ? getCurveColor(p.id, 0) : "url(#chgrad" + p.id + ")", + ); + t.selectAll() + .data((p) => (p.isTarget ? [] : LR)) + .join("text") + .attr("class", "keyCLabel") + .attrs({ + x: 17 + keyExt, + y: (_, i) => 12 * (i - (LR.length - 1) / 2), + dy: "0.32em", + "text-anchor": "start", + "font-size": 10.5, + }) + .text((t) => t); + t.filter((p) => p.isTarget) + .append("text") + .attrs( + keyExt ? { x: 7, y: 6, "text-anchor": "middle" } : { x: 17, y: 0, "text-anchor": "start" }, + ) + .attrs({ dy: "0.32em", "font-size": 8, fill: (p) => getCurveColor(p.id, 0) }) + .text("Target"); + let uchl = (f) => + function (p) { + updateCurves(p, f(p)); + hl(p, true); + }; + s.append("rect") + .attr("class", "keySelBoth") + .attrs({ x: 40 + channelbox_x(0), width: 40, height: 12, opacity: 0, display: "none" }) + .on( + "click", + uchl((p) => 0), + ); + s.append("g") + .attr("class", "keySel") + .attr("transform", (p) => channelbox_tr(p.avg)) + .on( + "click", + uchl((p) => !p.avg), + ) + .selectAll() + .data([0, 80]) + .join("rect") + .attrs({ x: (d) => d, y: -12, width: 40, height: 24, opacity: 0 }); + let o = s + .filter((p) => !notMultichannel(p)) + .selectAll() + .data((p) => [ + [p, 0], + [p, 1], + ]) + .join("g") + .attr("class", "keyOnly") + .attr("transform", (pi) => "translate(25," + [-6, 6][pi[1]] + ")") + .call( + setHover, + (h) => + function (pi) { + let p = pi[0], + cs = p.activeCurves; + if (!p.hide && cs.length === 2) { + d3.event.stopPropagation(); + hl(p, h ? (c) => c === cs[pi[1]] : true); + clearLabels(); + gpath + .selectAll("path") + .filter((c) => c.p === p) + .attr("opacity", h ? (c) => (c !== cs[pi[1]] ? 0.7 : null) : null); + } + }, + ) + .on("click", (pi) => updateCurves(pi[0], false, pi[1])); + o.append("rect").attrs({ x: 0, y: -6, width: 30, height: 12, opacity: 0 }); + o.append("text") + .attrs({ x: 0, y: 0, dy: "0.28em", "text-anchor": "start", "font-size": 7.5 }) + .text("only"); + s.append("text") + .attr("class", "imbalance") + .attrs({ x: 8, y: 0, dy: "0.35em", "font-size": 10.5 }) + .text("!"); + if (sampnums.length > 1) { + let a = s.filter((p) => !p.isTarget); + let f = LR.length > 1 ? (n) => "all " + n : (n) => n + " samples"; + let t = a + .selectAll() + .data((p) => + ["AVG", f(Math.floor(validChannels(p).length / LR.length))].map((t, i) => [ + t, + i === +p.samp ? 1 : 0.6, + ]), + ) + .join("text") + .attr("class", "keySamp") + .attrs({ + x: -18.5 - keyLeft, + y: (_, i) => 12 * (i - 1 / 2), + dy: "0.33em", + "text-anchor": "start", + "font-size": 7, + opacity: (t) => t[1], + }) + .text((t) => t[0]); + a.append("rect") + .attrs({ x: -19 - keyLeft, y: -12, width: keyLeft ? 16 : 38, height: 24, opacity: 0 }) + .on("click", (p) => updateCurves(p, undefined, p.lr, !p.samp)); + } + updateKey(s); } function updateKey(s) { - let disp = fn => e => e.attr("display",p=>fn(p)?null:"none"), - cs = hasChannelSel; - s.select(".imbalance").call(disp(hasImbalance)); - s.select(".keySel").call(disp(p=>cs(p))); - s.selectAll(".keyOnly").call(disp(pi=>cs(pi[0]))); - s.selectAll(".keyCLabel").data(p=>p.channels).call(disp(c=>c)); - s.select("g").attr("mask",p=>cs(p)?"url(#chmask"+p.id+")":null); - let l=-17-(keyLeft?8:0); - s.select("path").attr("d", p => - notMultichannel(p) ? "M"+(15+keyExt)+" 0H"+l : - ["M15 -6H9C0 -6,0 0,-9 0H"+l,"M"+l+" 0H-9C0 0,0 6,9 6H15"] - .filter((_,i) => p.channels[i]) - .reduce((a,b) => a+b.slice(6)) - ); + let disp = (fn) => (e) => e.attr("display", (p) => (fn(p) ? null : "none")), + cs = hasChannelSel; + s.select(".imbalance").call(disp(hasImbalance)); + s.select(".keySel").call(disp((p) => cs(p))); + s.selectAll(".keyOnly").call(disp((pi) => cs(pi[0]))); + s.selectAll(".keyCLabel") + .data((p) => p.channels) + .call(disp((c) => c)); + s.select("g").attr("mask", (p) => (cs(p) ? "url(#chmask" + p.id + ")" : null)); + let l = -17 - (keyLeft ? 8 : 0); + s.select("path").attr("d", (p) => + notMultichannel(p) + ? "M" + (15 + keyExt) + " 0H" + l + : ["M15 -6H9C0 -6,0 0,-9 0H" + l, "M" + l + " 0H-9C0 0,0 6,9 6H15"] + .filter((_, i) => p.channels[i]) + .reduce((a, b) => a + b.slice(6)), + ); } function addModel(t) { - let n = t.append("div").attr("class","phonename").text(p=>p.dispName); - t.filter(p=>p.fileNames) - .append("div").attr("class","variants") - .call(function (s) { - s.append("svg").attr("viewBox","0 -2 10 11") - .append("path").attr("fill","currentColor") - .attr("d","M1 2L5 6L9 2L8 1L6 3Q5 4 4 3L2 1Z"); - }) - .attr("tabindex",0) // Make focusable - .on("focus", function (p) { - if (p.selectInProgress) return; - p.selectInProgress = true; - p.vars[p.fileName] = p.rawChannels; - d3.select(this) - .on("mousedown", function () { - d3.event.preventDefault(); - this.blur(); - }) - .select("path").attr("transform","translate(0,7)scale(1,-1)"); - let n = d3.select(this.parentElement).select(".phonename"); - n.text(""); - let q = p.copyOf || p, - o = q.objs || [p], - active_fns = o.map(v=>v.fileName), - vars = p.fileNames.map((f,i) => { - let j = active_fns.indexOf(f); - return j!==-1 ? o[j] : - {fileName:f, dispName:q.dispNames[i]}; - }); - let nVariantNames = n.append("div").attr("class","variant-names"); - let nVariantPopouts = n.append("div").attr("class","variant-popouts"); - let d = nVariantNames.selectAll().data(vars).join("div") - .attr("class","variantName").text(v=>v.dispName), - w = d3.max(d.nodes(), d=>d.getBoundingClientRect().width); - d.style("width",w+"px"); - d.filter(v=>v.active) - .style("cursor","initial") - .style("color", getTextColor) - .call(setHover, h => p => - table.selectAll("tr").filter(q=>q===p) - .classed("highlight", h) - ); - let c = nVariantPopouts.selectAll().data(vars).join("span") - .html(" + ").attr("class","variantPopout") - .style("left",(w+5)+"px") - .style("display",v=>v.active?"none":null); - [d,c].forEach(e=>e.transition().style("top",(_,i)=>i*1.3+"em")); - d.filter(v=>!v.active).on("mousedown", v => Object.assign(p,v)); - c.on("mousedown", function (v) { - showVariant(q, v); - }); + let n = t + .append("div") + .attr("class", "phonename") + .text((p) => p.dispName); + t.filter((p) => p.fileNames) + .append("div") + .attr("class", "variants") + .call(function (s) { + s.append("svg") + .attr("viewBox", "0 -2 10 11") + .append("path") + .attr("fill", "currentColor") + .attr("d", "M1 2L5 6L9 2L8 1L6 3Q5 4 4 3L2 1Z"); + }) + .attr("tabindex", 0) // Make focusable + .on("focus", function (p) { + if (p.selectInProgress) return; + p.selectInProgress = true; + p.vars[p.fileName] = p.rawChannels; + d3.select(this) + .on("mousedown", function () { + d3.event.preventDefault(); + this.blur(); }) - .on("blur", function endSelect(p) { - if (document.activeElement === this) return; - p.selectInProgress = false; - d3.select(this) - .on("mousedown", null) - .select("path").attr("transform", null); - let n = d3.select(this.parentElement).select(".phonename"); - n.selectAll("div") - .call(setHover, h=>p=>null) - .transition().style("top",0+"em").remove() - .end().then(()=>n.text(p=>p.dispName)); - changeVariant(p, updateVariant); - table.selectAll("tr").classed("highlight", false); // Prevents some glitches + .select("path") + .attr("transform", "translate(0,7)scale(1,-1)"); + let n = d3.select(this.parentElement).select(".phonename"); + n.text(""); + let q = p.copyOf || p, + o = q.objs || [p], + active_fns = o.map((v) => v.fileName), + vars = p.fileNames.map((f, i) => { + let j = active_fns.indexOf(f); + return j !== -1 ? o[j] : { fileName: f, dispName: q.dispNames[i] }; }); - t.filter(p=>p.isTarget).append("span").text(" Target"); + let nVariantNames = n.append("div").attr("class", "variant-names"); + let nVariantPopouts = n.append("div").attr("class", "variant-popouts"); + let d = nVariantNames + .selectAll() + .data(vars) + .join("div") + .attr("class", "variantName") + .text((v) => v.dispName), + w = d3.max(d.nodes(), (d) => d.getBoundingClientRect().width); + d.style("width", w + "px"); + d.filter((v) => v.active) + .style("cursor", "initial") + .style("color", getTextColor) + .call( + setHover, + (h) => (p) => + table + .selectAll("tr") + .filter((q) => q === p) + .classed("highlight", h), + ); + let c = nVariantPopouts + .selectAll() + .data(vars) + .join("span") + .html(" + ") + .attr("class", "variantPopout") + .style("left", w + 5 + "px") + .style("display", (v) => (v.active ? "none" : null)); + [d, c].forEach((e) => e.transition().style("top", (_, i) => i * 1.3 + "em")); + d.filter((v) => !v.active).on("mousedown", (v) => Object.assign(p, v)); + c.on("mousedown", function (v) { + showVariant(q, v); + }); + }) + .on("blur", function endSelect(p) { + if (document.activeElement === this) return; + p.selectInProgress = false; + d3.select(this).on("mousedown", null).select("path").attr("transform", null); + let n = d3.select(this.parentElement).select(".phonename"); + n.selectAll("div") + .call(setHover, (h) => (p) => null) + .transition() + .style("top", 0 + "em") + .remove() + .end() + .then(() => n.text((p) => p.dispName)); + changeVariant(p, updateVariant); + table.selectAll("tr").classed("highlight", false); // Prevents some glitches + }); + t.filter((p) => p.isTarget) + .append("span") + .text(" Target"); } function updateVariant(p) { - updateKey(table.selectAll("tr").filter(q=>q===p).select(".keyLine")); - normalizePhone(p); - updatePaths(); + updateKey( + table + .selectAll("tr") + .filter((q) => q === p) + .select(".keyLine"), + ); + normalizePhone(p); + updatePaths(); } function changeVariant(p, update, trigger) { - let fn = p.fileName, - ch = p.vars[fn]; - function set(ch) { - p.rawChannels = ch; p.smooth = undefined; - smoothPhone(p); - setCurves(p); - update(p, 0, 0, trigger); - } - if (ch) { - set(ch); - } else { - loadFiles(p, set); - } + let fn = p.fileName, + ch = p.vars[fn]; + function set(ch) { + p.rawChannels = ch; + p.smooth = undefined; + smoothPhone(p); + setCurves(p); + update(p, 0, 0, trigger); + } + if (ch) { + set(ch); + } else { + loadFiles(p, set); + } } function showVariant(p, c, trigger) { - if (cantCompare(activePhones)) return; - if (!p.objs) { p.objs = [p]; } - p.objs.push(c); - c.active=true; c.copyOf=p; - ["brand","dispBrand","fileNames","vars"].map(k=>c[k]=p[k]); - changeVariant(c, showPhone, trigger); + if (cantCompare(activePhones)) return; + if (!p.objs) { + p.objs = [p]; + } + p.objs.push(c); + c.active = true; + c.copyOf = p; + ["brand", "dispBrand", "fileNames", "vars"].map((k) => (c[k] = p[k])); + changeVariant(c, showPhone, trigger); } function cpCircles(svg) { - svg.selectAll("circle") - .data(p => [[3,3,2],[6.6,4,1]].map(([cx,cy,r])=>({cx,cy,r,fill:getBgColor(p)}))) - .join("circle").attrs(d=>d); + svg + .selectAll("circle") + .data((p) => + [ + [3, 3, 2], + [6.6, 4, 1], + ].map(([cx, cy, r]) => ({ cx, cy, r, fill: getBgColor(p) })), + ) + .join("circle") + .attrs((d) => d); } function addColorPicker(svg) { - svg.attr("viewBox","0 0 9 5.3"); - svg.append("rect").attrs({x:0,y:0,width:9,height:5.3,fill:"none"}); - svg.call(cpCircles); - makeColorPicker(svg); + svg.attr("viewBox", "0 0 9 5.3"); + svg.append("rect").attrs({ x: 0, y: 0, width: 9, height: 5.3, fill: "none" }); + svg.call(cpCircles); + makeColorPicker(svg); } function makeColorPicker(elt) { - elt.on("click", function (p) { - p.id = getPhoneNumber(); - colorPhones(); - d3.event.stopPropagation(); - }); + elt.on("click", function (p) { + p.id = getPhoneNumber(); + colorPhones(); + d3.event.stopPropagation(); + }); } function colorPhones() { - updatePaths(); - let c = p=>p.active?getDivColor(p.id,true):null; - doc.select("#phones").selectAll("div.phone-item") - .style("background",c).style("border-color",c); - let t = table.selectAll("tr").filter(p=>!p.isTarget) - .style("color", c); - t.select("button").style("background-color",p=>getCurveColor(p.id,0)); - t= t.call(s => s.select(".remove").style("background-image",colorBar) - .select("svg").call(cpCircles)) - .select("td.channels"); // Key line - t.select("svg").remove(); - t.append("svg").call(addKey); -} - -let f_values = (function() { - // Standard frequencies, all phone need to interpolate to this - let f = [20]; - let step = Math.pow(2, 1/48); // 1/48 octave - while (f[f.length-1] < 20000) { f.push(f[f.length-1] * step) } - return f; + updatePaths(); + let c = (p) => (p.active ? getDivColor(p.id, true) : null); + doc.select("#phones").selectAll("div.phone-item").style("background", c).style("border-color", c); + let t = table + .selectAll("tr") + .filter((p) => !p.isTarget) + .style("color", c); + t.select("button").style("background-color", (p) => getCurveColor(p.id, 0)); + t = t + .call((s) => + s.select(".remove").style("background-image", colorBar).select("svg").call(cpCircles), + ) + .select("td.channels"); // Key line + t.select("svg").remove(); + t.append("svg").call(addKey); +} + +let f_values = (function () { + // Standard frequencies, all phone need to interpolate to this + let f = [20]; + let step = Math.pow(2, 1 / 48); // 1/48 octave + while (f[f.length - 1] < 20000) { + f.push(f[f.length - 1] * step); + } + return f; })(); -let fr_to_ind = fr => d3.bisect(f_values, fr, 0, f_values.length-1); +let fr_to_ind = (fr) => d3.bisect(f_values, fr, 0, f_values.length - 1); function range_to_slice(xs, fn) { - let r = xs.map(v => d3.bisectLeft(f_values, x.invert(fn(v)))); - return a => a.slice(Math.max(r[0],0), r[1]+1); + let r = xs.map((v) => d3.bisectLeft(f_values, x.invert(fn(v)))); + return (a) => a.slice(Math.max(r[0], 0), r[1] + 1); } -let norm_sel = ( default_normalization.toLowerCase() === "db" ) ? 0:1, - norm_fr = default_norm_hz, - norm_phon = default_norm_db; +let norm_sel = default_normalization.toLowerCase() === "db" ? 0 : 1, + norm_fr = default_norm_hz, + norm_phon = default_norm_db; function normalizePhone(p) { - if (norm_sel) { // fr - let i = fr_to_ind(norm_fr); - let avg = l => 20*Math.log10(d3.mean(l, d=>Math.pow(10,d/20))); - p.norm = 60 - avg(validChannels(p).map(l=>l[i][1])); - } else { // phon - p.norm = find_offset(getAvg(p), norm_phon); - } - if (p.eq) { - p.eq.norm = p.norm; // copy parent's norm to child - } else if (p.eqParent) { - p.norm = p.eqParent.norm; // set child's norm from parent - } + if (norm_sel) { + // fr + let i = fr_to_ind(norm_fr); + let avg = (l) => 20 * Math.log10(d3.mean(l, (d) => Math.pow(10, d / 20))); + p.norm = 60 - avg(validChannels(p).map((l) => l[i][1])); + } else { + // phon + p.norm = find_offset(getAvg(p), norm_phon); + } + if (p.eq) { + p.eq.norm = p.norm; // copy parent's norm to child + } else if (p.eqParent) { + p.norm = p.eqParent.norm; // set child's norm from parent + } } let norms = doc.select(".normalize").selectAll("div"); -norms.classed("selected",(_,i)=>i===norm_sel); +norms.classed("selected", (_, i) => i === norm_sel); function setNorm(_, i, change) { - if (change !== false) { - if (!this.checkValidity()) return; - let v = +this.value; - if (i) { norm_fr=v; } else { norm_phon=v; } - } - norm_sel = i; - norms.classed("selected",(_,i)=>i===norm_sel); - activePhones.forEach(normalizePhone); - if (baseline.p) { baseline = getBaseline(baseline.p); } - updateYCenter(); - - if (!userConfigApplicationActive) { - setUserConfig(); - updatePaths(); + if (change !== false) { + if (!this.checkValidity()) return; + let v = +this.value; + if (i) { + norm_fr = v; } else { - updatePaths("config"); - } + norm_phon = v; + } + } + norm_sel = i; + norms.classed("selected", (_, i) => i === norm_sel); + activePhones.forEach(normalizePhone); + if (baseline.p) { + baseline = getBaseline(baseline.p); + } + updateYCenter(); + + if (!userConfigApplicationActive) { + setUserConfig(); + updatePaths(); + } else { + updatePaths("config"); + } } -norms.select("input") - .on("change input",setNorm) - .on("keypress", function(_, i) { - if (d3.event.key==="Enter") { setNorm.bind(this)(_,i); } - }); -norms.select("span").on("click", (_,i)=>setNorm(_,i,false)); +norms + .select("input") + .on("change input", setNorm) + .on("keypress", function (_, i) { + if (d3.event.key === "Enter") { + setNorm.bind(this)(_, i); + } + }); +norms.select("span").on("click", (_, i) => setNorm(_, i, false)); let addPhoneSet = false, // Whether add phone button was clicked - addPhoneLock= false; + addPhoneLock = false; function setAddButton(a) { - if (a && cantCompare(activePhones)) return false; - if (addPhoneSet !== a) { - addPhoneSet = a; - doc.select(".addPhone").classed("selected", a) - .classed("locked", addPhoneLock &= a); - } - return true; -} -doc.select(".addPhone").selectAll("td") - .on("click", ()=>setAddButton(!addPhoneSet)); + if (a && cantCompare(activePhones)) return false; + if (addPhoneSet !== a) { + addPhoneSet = a; + doc + .select(".addPhone") + .classed("selected", a) + .classed("locked", (addPhoneLock &= a)); + } + return true; +} +doc + .select(".addPhone") + .selectAll("td") + .on("click", () => setAddButton(!addPhoneSet)); doc.select(".addLock").on("click", function () { - d3.event.preventDefault(); - let on = !addPhoneLock; - if (!setAddButton(on)) return; - if (on) { - doc.select(".addPhone").classed("locked", addPhoneLock=true); - } + d3.event.preventDefault(); + let on = !addPhoneLock; + if (!setAddButton(on)) return; + if (on) { + doc.select(".addPhone").classed("locked", (addPhoneLock = true)); + } }); function showPhone(p, exclusive, suppressVariant, trigger) { - if (p.isTarget && activePhones.indexOf(p)!==-1) { - removePhone(p); - return; - } - if (p.isTarget) { - exclusive = false; - } - if (addPhoneSet) { - exclusive = false; - if (!addPhoneLock || cantCompare(activePhones,1,null,true)) { - setAddButton(false); - } - } - let keep = !exclusive ? (q=>true) - : (q => q.copyOf===p || q.pin || q.isTarget!==p.isTarget); - if (cantCompare(activePhones.filter(keep),0, p)) return; - if (!p.rawChannels) { - loadFiles(p, function (ch) { - if (p.rawChannels) return; - p.rawChannels = ch; - showPhone(p, exclusive, suppressVariant, trigger); - - // Scroll to selected - if (trigger) { scrollToActive(); } - - // Analytics event - if (analyticsEnabled) { pushPhoneTag("phone_displayed", p, trigger); } - }); - return; - } - smoothPhone(p); - if (p.id === undefined) { p.id = getPhoneNumber(); } - normalizePhone(p); p.offset=p.offset||0; - if (exclusive) { - activePhones = activePhones.filter(q => q.active = keep(q)); - if (baseline.p && !baseline.p.active) setBaseline(baseline0,1); - } - if (activePhones.indexOf(p)===-1 && (suppressVariant || !p.objs)) { - let avg = false; - if (!p.isTarget) { - let ap = activePhones.filter(p => !p.isTarget); - avg = ap.length >= 1; - if (ap.length===1 && ap[0].activeCurves.length!==1) { - setCurves(ap[0], true); - } - activePhones.push(p); - } else { - activePhones.unshift(p); - } - p.active = true; - setCurves(p, avg); - } - updatePaths(trigger); - updatePhoneTable(trigger); - d3.selectAll("#phones .phone-item,.target") - .filter(p=>p.id!==undefined) - .call(setPhoneTr); - //Displays variant pop-up when phone displayed - if (!suppressVariant && p.fileNames && !p.copyOf && window.innerWidth > 1000) { - table.selectAll("tr").filter(q=>q===p).select(".variants").node().focus(); + if (p.isTarget && activePhones.indexOf(p) !== -1) { + removePhone(p); + return; + } + if (p.isTarget) { + exclusive = false; + } + if (addPhoneSet) { + exclusive = false; + if (!addPhoneLock || cantCompare(activePhones, 1, null, true)) { + setAddButton(false); + } + } + let keep = !exclusive ? (q) => true : (q) => q.copyOf === p || q.pin || q.isTarget !== p.isTarget; + if (cantCompare(activePhones.filter(keep), 0, p)) return; + if (!p.rawChannels) { + loadFiles(p, function (ch) { + if (p.rawChannels) return; + p.rawChannels = ch; + showPhone(p, exclusive, suppressVariant, trigger); + + // Scroll to selected + if (trigger) { + scrollToActive(); + } + + // Analytics event + if (analyticsEnabled) { + pushPhoneTag("phone_displayed", p, trigger); + } + }); + return; + } + smoothPhone(p); + if (p.id === undefined) { + p.id = getPhoneNumber(); + } + normalizePhone(p); + p.offset = p.offset || 0; + if (exclusive) { + activePhones = activePhones.filter((q) => (q.active = keep(q))); + if (baseline.p && !baseline.p.active) setBaseline(baseline0, 1); + } + if (activePhones.indexOf(p) === -1 && (suppressVariant || !p.objs)) { + let avg = false; + if (!p.isTarget) { + let ap = activePhones.filter((p) => !p.isTarget); + avg = ap.length >= 1; + if (ap.length === 1 && ap[0].activeCurves.length !== 1) { + setCurves(ap[0], true); + } + activePhones.push(p); } else { - document.activeElement.blur(); - } - if (extraEnabled && extraEQEnabled) { - updateEQPhoneSelect(); - } - if (!p.isTarget && alt_augment ) { augmentList(p); } - - // Apply user config view settings - if (typeof trigger !== "undefined") { - userConfigApplyViewSettings(p.fileName); - } + activePhones.unshift(p); + } + p.active = true; + setCurves(p, avg); + } + updatePaths(trigger); + updatePhoneTable(trigger); + d3.selectAll("#phones .phone-item,.target") + .filter((p) => p.id !== undefined) + .call(setPhoneTr); + //Displays variant pop-up when phone displayed + if (!suppressVariant && p.fileNames && !p.copyOf && window.innerWidth > 1000) { + table + .selectAll("tr") + .filter((q) => q === p) + .select(".variants") + .node() + .focus(); + } else { + document.activeElement.blur(); + } + if (extraEnabled && extraEQEnabled) { + updateEQPhoneSelect(); + } + if (!p.isTarget && alt_augment) { + augmentList(p); + } + + // Apply user config view settings + if (typeof trigger !== "undefined") { + userConfigApplyViewSettings(p.fileName); + } } function removeCopies(p) { - if (p.objs) { - p.objs.forEach(q=>q.active=false); - delete p.objs; - } - removePhone(p); + if (p.objs) { + p.objs.forEach((q) => (q.active = false)); + delete p.objs; + } + removePhone(p); } function removePhone(p) { - p.active = p.pin = false; nextPN = null; - activePhones = activePhones.filter(q => q.active); - if (!p.isTarget) { - let ap = activePhones.filter(p => !p.isTarget); - if (ap.length === 1) { - setCurves(ap[0], false); - } - } - updatePaths(); - if (baseline.p && !baseline.p.active) { setBaseline(baseline0); } - updatePhoneTable(); - d3.selectAll("#phones div,.target") - .filter(q=>q===(p.copyOf||p)) - .call(setPhoneTr); - if (extraEnabled && extraEQEnabled) { - updateEQPhoneSelect(); - } + p.active = p.pin = false; + nextPN = null; + activePhones = activePhones.filter((q) => q.active); + if (!p.isTarget) { + let ap = activePhones.filter((p) => !p.isTarget); + if (ap.length === 1) { + setCurves(ap[0], false); + } + } + updatePaths(); + if (baseline.p && !baseline.p.active) { + setBaseline(baseline0); + } + updatePhoneTable(); + d3.selectAll("#phones div,.target") + .filter((q) => q === (p.copyOf || p)) + .call(setPhoneTr); + if (extraEnabled && extraEQEnabled) { + updateEQPhoneSelect(); + } } function asPhoneObj(b, p, isInit, inits) { - if (!isInit) { - isInit = _ => false; - } - let r = { brand:b, dispBrand:b.name }; - if (typeof p === "string") { - r.phone = r.fileName = p; - if (isInit(p)) inits.push(r); + if (!isInit) { + isInit = (_) => false; + } + let r = { brand: b, dispBrand: b.name }; + if (typeof p === "string") { + r.phone = r.fileName = p; + if (isInit(p)) inits.push(r); + } else { + r.phone = p.name; + if (p.collab) { + r.dispBrand += " x " + p.collab; + r.collab = brandMap[p.collab]; + } + let f = p.file || p.name; + if (typeof f === "string") { + r.fileName = f; + if (isInit(f)) inits.push(r); } else { - r.phone = p.name; - if (p.collab) { - r.dispBrand += " x "+p.collab; - r.collab = brandMap[p.collab]; - } - let f = p.file || p.name; - if (typeof f === "string") { - r.fileName = f; - if (isInit(f)) inits.push(r); - } else { - r.fileNames = f; - r.vars = {}; - let dns = f; - if (p.suffix) { - dns = p.suffix.map( - s => p.name + (s ? " "+s : "") - ); - } else if (p.prefix) { - let reg = new RegExp("^"+p.prefix+"\s*", "i"); - dns = f.map(n => { - n = n.replace(reg, ""); - return p.name + (n.length ? " "+n : n); - }); - } - r.dispNames = dns; - r.fileName = f[0]; - r.dispName = dns[0]; - let c = r; - f.map((fn,i) => { - if (!isInit(fn)) return; - c.fileName=fn; c.dispName=dns[i]; - inits.push(c); - c = {copyOf:r}; - }); - } - } - r.dispName = r.dispName || r.phone; - r.fullName = r.dispBrand + " " + r.phone; - if (alt_augment) { - r.reviewScore = p.reviewScore; - r.reviewLink = p.reviewLink; - r.shopLink = p.shopLink; - r.price = p.price; - } - return r; -} - -d3.json(typeof PHONE_BOOK !== "undefined" ? PHONE_BOOK - : DIR+"phone_book.json?"+ new Date().getTime()).then(function (brands) { - let brandMap = window.brandMap = {}, - inits = [], - initReq = typeof init_phones !== "undefined" ? [init_phones].flat() : false; - loadFromShare = 0; - - if (ifURL) { - let url = targetWindow.location.href, - par = "share="; - emb = "embed"; - baseURL = url.split("?").shift(); - - if (url.includes(par) && url.includes(emb)) { - initReq = decodeURIComponent(url.replace(/_/g," ").split(par).pop()).split(","); - loadFromShare = 2; - - setModeEmbed(); - } else if (url.includes(par)) { - initReq = decodeURIComponent(url.replace(/_/g," ").split(par).pop()).split(","); - loadFromShare = 1; - } else if (url.includes(emb)) { - setModeEmbed(); - } - } - - // Apply user config to inits - userConfigAppendInits(initReq); - - let isInit = initReq ? f => initReq.indexOf(f) !== -1 - : _ => false; - - if (loadFromShare === 1) { - initMode = "share"; - } else if (loadFromShare === 2) { - initMode = "embed"; - } else { - initMode = "config"; - } - - brands.push({ name: "Uploaded", phones: [] }); - brands.forEach(b => brandMap[b.name] = b); - brands.forEach(function (b) { - b.active = false; - b.phoneObjs = b.phones.map(function (p) { - return asPhoneObj(b, p, isInit, inits); + r.fileNames = f; + r.vars = {}; + let dns = f; + if (p.suffix) { + dns = p.suffix.map((s) => p.name + (s ? " " + s : "")); + } else if (p.prefix) { + let reg = new RegExp("^" + p.prefix + "\s*", "i"); + dns = f.map((n) => { + n = n.replace(reg, ""); + return p.name + (n.length ? " " + n : n); }); - }); - - let allPhones = window.allPhones = d3.merge(brands.map(b=>b.phoneObjs)), - currentBrands = []; - if (!initReq) inits.push(allPhones[0]); - - function setClicks(fn) { return function (elt) { - elt .on("mousedown", () => d3.event.preventDefault()) - .on("click", p => fn(p,!d3.event.ctrlKey)) - .on("auxclick", p => d3.event.button===1 ? fn(p,0) : 0); - }; } - - let brandSel = doc.select("#brands").selectAll() - .data(brands).join("div") - .text(b => b.name + (b.suffix?" "+b.suffix:"")) - .call(setClicks(setBrand)); - - let bg = (h,fn) => function (p) { - d3.select(this).style("background", fn(p)); - (p.objs||[p]).forEach(q=>hl(q,h)); + } + r.dispNames = dns; + r.fileName = f[0]; + r.dispName = dns[0]; + let c = r; + f.map((fn, i) => { + if (!isInit(fn)) return; + c.fileName = fn; + c.dispName = dns[i]; + inits.push(c); + c = { copyOf: r }; + }); } - window.updatePhoneSelect = () => { - doc.select("#phones").selectAll("div.phone-item") - .data(allPhones) - .join((enter) => { - let phoneDiv = enter.append("div") - .attr("class","phone-item") - .attr("name", p=>p.fullName) - .on("mouseover", bg(true, p => getDivColor(p.id===undefined?nextPhoneNumber():p.id, true))) - .on("mouseout" , bg(false,p => p.id!==undefined?getDivColor(p.id,p.active):null)) - .call(setClicks(showPhone)); - phoneDiv.append("span").text(p=>p.fullName); - // Adding the + selection button - phoneDiv.append("div") - .attr("class", "phone-item-add") - .on("click", p => { - d3.event.stopPropagation(); - showPhone(p, 0); - }); - }); + } + r.dispName = r.dispName || r.phone; + r.fullName = r.dispBrand + " " + r.phone; + if (alt_augment) { + r.reviewScore = p.reviewScore; + r.reviewLink = p.reviewLink; + r.shopLink = p.shopLink; + r.price = p.price; + } + return r; +} + +d3.json( + typeof PHONE_BOOK !== "undefined" ? PHONE_BOOK : DIR + "phone_book.json?" + new Date().getTime(), +).then(function (brands) { + let brandMap = (window.brandMap = {}), + inits = [], + initReq = typeof init_phones !== "undefined" ? [init_phones].flat() : false; + loadFromShare = 0; + + if (ifURL) { + let url = targetWindow.location.href, + par = "share="; + emb = "embed"; + baseURL = url.split("?").shift(); + + if (url.includes(par) && url.includes(emb)) { + initReq = decodeURIComponent(url.replace(/_/g, " ").split(par).pop()).split(","); + loadFromShare = 2; + + setModeEmbed(); + } else if (url.includes(par)) { + initReq = decodeURIComponent(url.replace(/_/g, " ").split(par).pop()).split(","); + loadFromShare = 1; + } else if (url.includes(emb)) { + setModeEmbed(); + } + } + + // Apply user config to inits + userConfigAppendInits(initReq); + + let isInit = initReq ? (f) => initReq.indexOf(f) !== -1 : (_) => false; + + if (loadFromShare === 1) { + initMode = "share"; + } else if (loadFromShare === 2) { + initMode = "embed"; + } else { + initMode = "config"; + } + + brands.push({ name: "Uploaded", phones: [] }); + brands.forEach((b) => (brandMap[b.name] = b)); + brands.forEach(function (b) { + b.active = false; + b.phoneObjs = b.phones.map(function (p) { + return asPhoneObj(b, p, isInit, inits); + }); + }); + + let allPhones = (window.allPhones = d3.merge(brands.map((b) => b.phoneObjs))), + currentBrands = []; + if (!initReq) inits.push(allPhones[0]); + + function setClicks(fn) { + return function (elt) { + elt + .on("mousedown", () => d3.event.preventDefault()) + .on("click", (p) => fn(p, !d3.event.ctrlKey)) + .on("auxclick", (p) => (d3.event.button === 1 ? fn(p, 0) : 0)); }; - updatePhoneSelect(); - - if (targets) { - let b = window.brandTarget = { name:"Targets", active:false }, - ti = -targets.length, - ph = t => ({ - isTarget:true, brand:b, - dispName:t, phone:t, fullName:t+" Target", fileName:t+" Target" - }); - d3.select(".manage").insert("div",".manageTable") - .attr("class", "targets collapseTools"); - let l = (text,c) => s => s.append("div").attr("class","targetLabel").append("span").text(text); - let ts = b.phoneObjs = doc.select(".targets").call(l("Targets")) - .selectAll().data(targets).join("div").call(l(t=>t.type)) - .style("flex-grow",t=>t.files.length).attr("class","targetClass") - .selectAll().data(t=>t.files.map(ph)) - .join("div").text(t=>t.dispName).attr("class","target") - .call(setClicks(showPhone)) - .data(); - ts.forEach((t,i) => { - t.id = i-ts.length; - if (isInit(t.fileName)) inits.push(t); - }); - } - - inits.map(p => p.copyOf ? showVariant(p.copyOf, p, initMode) - : showPhone(p,0,1, initMode)); - - function setBrand(b, exclusive) { - let phoneSel = doc.select("#phones").selectAll("div.phone-item"); - let incl = currentBrands.indexOf(b) !== -1; - let hasBrand = (p,b) => p.brand===b || p.collab===b; - if (exclusive || currentBrands.length===0) { - currentBrands.forEach(br => br.active = false); - if (incl) { - currentBrands = []; - phoneSel.style("display", null); - phoneSel.select("span").text(p=>p.fullName); - } else { - currentBrands = [b]; - phoneSel.style("display", p => hasBrand(p,b)?null:"none"); - phoneSel.filter(p => hasBrand(p,b)).select("span").text(p=>p.phone); - } - } else { - if (incl) return; - if (currentBrands.length === 1) { - phoneSel.select("span").text(p=>p.fullName); - } - currentBrands.push(b); - phoneSel.filter(p => hasBrand(p,b)).style("display", null); - } - if (!incl) b.active = true; - brandSel.classed("active", br => br.active); - } - - let phoneSearch = new Fuse( - allPhones, - { - shouldSort: false, - tokenize: false, - threshold: 0.2, - minMatchCharLength: 2, - keys: [ - {weight:0.3, name:"dispBrand"}, - {weight:0.1, name:"brand.suffix"}, - {weight:0.6, name:"phone"} - ] - } - ); - let brandSearch = new Fuse( - brands, - { - shouldSort: false, - tokenize: false, - threshold: 0.05, - minMatchCharLength: 3, - keys: [ - {weight:0.9, name:"name"}, - {weight:0.1, name:"suffix"}, - ] - } - ); - doc.select(".search").on("input", function () { - //d3.select(this).attr("placeholder",null); - let fn, bl = brands; - let c = currentBrands; - let test = p => c.indexOf(p.brand )!==-1 - || c.indexOf(p.collab)!==-1; - if (this.value.length > 1) { - let s = phoneSearch.search(this.value), - t = c.length ? s.filter(test) : s; - if (t.length) s = t; - fn = p => s.indexOf(p)!==-1; - let b = brandSearch.search(this.value); - if (b.length) bl = b; - } else { - fn = c.length ? test : (p=>true); - } - let phoneSel = doc.select("#phones").selectAll("div.phone-item"); - phoneSel.style("display", p => fn(p)?null:"none"); - brandSel.style("display", b => bl.indexOf(b)!==-1?null:"none"); + } + + let brandSel = doc + .select("#brands") + .selectAll() + .data(brands) + .join("div") + .text((b) => b.name + (b.suffix ? " " + b.suffix : "")) + .call(setClicks(setBrand)); + + let bg = (h, fn) => + function (p) { + d3.select(this).style("background", fn(p)); + (p.objs || [p]).forEach((q) => hl(q, h)); + }; + window.updatePhoneSelect = () => { + doc + .select("#phones") + .selectAll("div.phone-item") + .data(allPhones) + .join((enter) => { + let phoneDiv = enter + .append("div") + .attr("class", "phone-item") + .attr("name", (p) => p.fullName) + .on( + "mouseover", + bg(true, (p) => getDivColor(p.id === undefined ? nextPhoneNumber() : p.id, true)), + ) + .on( + "mouseout", + bg(false, (p) => (p.id !== undefined ? getDivColor(p.id, p.active) : null)), + ) + .call(setClicks(showPhone)); + phoneDiv.append("span").text((p) => p.fullName); + // Adding the + selection button + phoneDiv + .append("div") + .attr("class", "phone-item-add") + .on("click", (p) => { + d3.event.stopPropagation(); + showPhone(p, 0); + }); + }); + }; + updatePhoneSelect(); + + if (targets) { + let b = (window.brandTarget = { name: "Targets", active: false }), + ti = -targets.length, + ph = (t) => ({ + isTarget: true, + brand: b, + dispName: t, + phone: t, + fullName: t + " Target", + fileName: t + " Target", + }); + d3.select(".manage").insert("div", ".manageTable").attr("class", "targets collapseTools"); + let l = (text, c) => (s) => + s.append("div").attr("class", "targetLabel").append("span").text(text); + let ts = (b.phoneObjs = doc + .select(".targets") + .call(l("Targets")) + .selectAll() + .data(targets) + .join("div") + .call(l((t) => t.type)) + .style("flex-grow", (t) => t.files.length) + .attr("class", "targetClass") + .selectAll() + .data((t) => t.files.map(ph)) + .join("div") + .text((t) => t.dispName) + .attr("class", "target") + .call(setClicks(showPhone)) + .data()); + ts.forEach((t, i) => { + t.id = i - ts.length; + if (isInit(t.fileName)) inits.push(t); }); + } - doc.select("#recolor").on("click", function () { - allPhones.forEach(p => { if (!p.isTarget) { delete p.id; } }); - phoneNumber = 0; nextPN = null; - activePhones.forEach(p => { if (!p.isTarget) { p.id = getPhoneNumber(); } }); - colorPhones(); + inits.map((p) => (p.copyOf ? showVariant(p.copyOf, p, initMode) : showPhone(p, 0, 1, initMode))); + + function setBrand(b, exclusive) { + let phoneSel = doc.select("#phones").selectAll("div.phone-item"); + let incl = currentBrands.indexOf(b) !== -1; + let hasBrand = (p, b) => p.brand === b || p.collab === b; + if (exclusive || currentBrands.length === 0) { + currentBrands.forEach((br) => (br.active = false)); + if (incl) { + currentBrands = []; + phoneSel.style("display", null); + phoneSel.select("span").text((p) => p.fullName); + } else { + currentBrands = [b]; + phoneSel.style("display", (p) => (hasBrand(p, b) ? null : "none")); + phoneSel + .filter((p) => hasBrand(p, b)) + .select("span") + .text((p) => p.phone); + } + } else { + if (incl) return; + if (currentBrands.length === 1) { + phoneSel.select("span").text((p) => p.fullName); + } + currentBrands.push(b); + phoneSel.filter((p) => hasBrand(p, b)).style("display", null); + } + if (!incl) b.active = true; + brandSel.classed("active", (br) => br.active); + } + + let phoneSearch = new Fuse(allPhones, { + shouldSort: false, + tokenize: false, + threshold: 0.2, + minMatchCharLength: 2, + keys: [ + { weight: 0.3, name: "dispBrand" }, + { weight: 0.1, name: "brand.suffix" }, + { weight: 0.6, name: "phone" }, + ], + }); + let brandSearch = new Fuse(brands, { + shouldSort: false, + tokenize: false, + threshold: 0.05, + minMatchCharLength: 3, + keys: [ + { weight: 0.9, name: "name" }, + { weight: 0.1, name: "suffix" }, + ], + }); + doc.select(".search").on("input", function () { + //d3.select(this).attr("placeholder",null); + let fn, + bl = brands; + let c = currentBrands; + let test = (p) => c.indexOf(p.brand) !== -1 || c.indexOf(p.collab) !== -1; + if (this.value.length > 1) { + let s = phoneSearch.search(this.value), + t = c.length ? s.filter(test) : s; + if (t.length) s = t; + fn = (p) => s.indexOf(p) !== -1; + let b = brandSearch.search(this.value); + if (b.length) bl = b; + } else { + fn = c.length ? test : (p) => true; + } + let phoneSel = doc.select("#phones").selectAll("div.phone-item"); + phoneSel.style("display", (p) => (fn(p) ? null : "none")); + brandSel.style("display", (b) => (bl.indexOf(b) !== -1 ? null : "none")); + }); + + doc.select("#recolor").on("click", function () { + allPhones.forEach((p) => { + if (!p.isTarget) { + delete p.id; + } }); - - doc.select("#theme").on("click", function () { - themeChooser("change"); + phoneNumber = 0; + nextPN = null; + activePhones.forEach((p) => { + if (!p.isTarget) { + p.id = getPhoneNumber(); + } }); - - userConfigApplyNormalization(); + colorPhones(); + }); + + doc.select("#theme").on("click", function () { + themeChooser("change"); + }); + + userConfigApplyNormalization(); }); let pathHoverTimeout; function pathHL(c, m, imm) { - gpath.selectAll("path").classed("highlight", c ? d=>d===c : false); - table.selectAll("tr") .classed("highlight", c ? p=>p===c.p : false); - if (pathHoverTimeout) { clearTimeout(pathHoverTimeout); } - if(!stickyLabels) { - clearLabels(); - pathHoverTimeout = - imm ? pathTooltip(c, m) : - c ? setTimeout(pathTooltip, 400, c, m) : - undefined; - } + gpath.selectAll("path").classed("highlight", c ? (d) => d === c : false); + table.selectAll("tr").classed("highlight", c ? (p) => p === c.p : false); + if (pathHoverTimeout) { + clearTimeout(pathHoverTimeout); + } + if (!stickyLabels) { + clearLabels(); + pathHoverTimeout = imm ? pathTooltip(c, m) : c ? setTimeout(pathTooltip, 400, c, m) : undefined; + } } function pathTooltip(c, m) { - let g = gr.selectAll(".lineLabel").data([c.id]) - .join("g").attr("class","lineLabel"); - let t = g.append("text") - .attrs({x:m[0], y:m[1]-6, fill:getTooltipColor(c)}) - .text(t=>t); - let b = t.node().getBBox(), - o = pad.l+W - b.width; - if (o < b.x) { t.attr("x",o); b.x=o; } - // Background - g.insert("rect", "text") - .attrs({x:b.x-1, y:b.y-1, width:b.width+2, height:b.height+2}); + let g = gr.selectAll(".lineLabel").data([c.id]).join("g").attr("class", "lineLabel"); + let t = g + .append("text") + .attrs({ x: m[0], y: m[1] - 6, fill: getTooltipColor(c) }) + .text((t) => t); + let b = t.node().getBBox(), + o = pad.l + W - b.width; + if (o < b.x) { + t.attr("x", o); + b.x = o; + } + // Background + g.insert("rect", "text").attrs({ + x: b.x - 1, + y: b.y - 1, + width: b.width + 2, + height: b.height + 2, + }); } let interactInspect = false; -let graphInteract = imm => function () { - let cs = d3.merge(activePhones.map(p=>p.hide?[]:p.activeCurves)); +let graphInteract = (imm) => + function () { + let cs = d3.merge(activePhones.map((p) => (p.hide ? [] : p.activeCurves))); if (!cs.length) return; let m = d3.mouse(this); if (interactInspect) { - let ind = fr_to_ind(x.invert(m[0])), - x1 = x(f_values[ind]), - x0 = ind>0 ? x(f_values[ind-1]) : x1, - sel= m[0]-x0 < x1-m[0], - xv = sel ? x0 : x1; - ind -= sel; - function init(e) { - e.attr("class","inspector"); - e.append("line").attrs({x1:0,x2:0, y1:pad.t,y2:pad.t+H}); - e.append("text").attr("class","insp_dB").attr("x",2); - } - let insp = gr.selectAll(".inspector").data([xv]) - .join(enter => enter.append("g").call(init)) - .attr("transform",xv=>"translate("+xv+",0)"); - let dB = insp.select(".insp_dB").text(f_values[ind]+" Hz"); - let cy = cs.map(c => [c, baseline.fn(c.l)[ind][1]+getOffset(c.p)]); - cy.sort((d,e) => d[1]-e[1]); - function newTooltip(t) { - t.attr("class","lineLabel") - .attr("fill",d=>getTooltipColor(d)); - t.append("text").attr("x",2).text(d=>d.id); - t.append("g").selectAll().data([0,1]) - .join("text") - .attr("x",-16) - .attr("text-anchor",i=>i?"start":"end"); - t.datum(function(){return this.getBBox();}); - t.insert("rect", "text") - .attrs(b=>({x:b.x-1, y:b.y-1, width:b.width+2, height:b.height+2})); - } - let tt = insp.selectAll(".lineLabel").data(cy.map(d=>d[0]), d=>d.id) - .join(enter => enter.insert("g","line").call(newTooltip)); - let start = tt.select("g").datum((_,i) => cy[i][1]) - .selectAll("text").data(d => { - let s=d<-0.05?"-":""; d=Math.abs(d)+0.05; - return [s+Math.floor(d)+".",Math.floor((d%1)*10)]; - }) - .text(t=>t) - .filter((_,i)=>i===0) - .nodes().map(n=>n.getBBox().x-2); - tt.select("rect") - .attrs((b,i)=>({x:b.x+start[i]-1, width:b.width-start[i]+2})); - // Now compute heights - let hm = d3.max(tt.data().map(b=>b.height)), - hh = (y.invert(0)-y.invert(hm-1))/2, - stack = []; - cy.map(d=>d[1]).forEach(function (h,i) { - let n = 1; - let overlap = s => h/n - s.h/s.n <= hh*(s.n+n); - let l = stack.length; - while (l && overlap(stack[--l])) { - let s = stack.pop(); - h += s.h; n += s.n; - } - stack.push({h:h, n:n}); + let ind = fr_to_ind(x.invert(m[0])), + x1 = x(f_values[ind]), + x0 = ind > 0 ? x(f_values[ind - 1]) : x1, + sel = m[0] - x0 < x1 - m[0], + xv = sel ? x0 : x1; + ind -= sel; + function init(e) { + e.attr("class", "inspector"); + e.append("line").attrs({ x1: 0, x2: 0, y1: pad.t, y2: pad.t + H }); + e.append("text").attr("class", "insp_dB").attr("x", 2); + } + let insp = gr + .selectAll(".inspector") + .data([xv]) + .join((enter) => enter.append("g").call(init)) + .attr("transform", (xv) => "translate(" + xv + ",0)"); + let dB = insp.select(".insp_dB").text(f_values[ind] + " Hz"); + let cy = cs.map((c) => [c, baseline.fn(c.l)[ind][1] + getOffset(c.p)]); + cy.sort((d, e) => d[1] - e[1]); + function newTooltip(t) { + t.attr("class", "lineLabel").attr("fill", (d) => getTooltipColor(d)); + t.append("text") + .attr("x", 2) + .text((d) => d.id); + t.append("g") + .selectAll() + .data([0, 1]) + .join("text") + .attr("x", -16) + .attr("text-anchor", (i) => (i ? "start" : "end")); + t.datum(function () { + return this.getBBox(); }); - let ch = d3.merge(stack.map((s,i) => { - let h = s.h/s.n - (s.n-1)*hh; - return d3.range(s.n).map(k => h+k*2*hh); + t.insert("rect", "text").attrs((b) => ({ + x: b.x - 1, + y: b.y - 1, + width: b.width + 2, + height: b.height + 2, })); - tt.attr("transform",(_,i) => "translate(0,"+(y(ch[i])+5)+")"); - dB.attr("y", y(ch[ch.length-1]+2*hh)+1); + } + let tt = insp + .selectAll(".lineLabel") + .data( + cy.map((d) => d[0]), + (d) => d.id, + ) + .join((enter) => enter.insert("g", "line").call(newTooltip)); + let start = tt + .select("g") + .datum((_, i) => cy[i][1]) + .selectAll("text") + .data((d) => { + let s = d < -0.05 ? "-" : ""; + d = Math.abs(d) + 0.05; + return [s + Math.floor(d) + ".", Math.floor((d % 1) * 10)]; + }) + .text((t) => t) + .filter((_, i) => i === 0) + .nodes() + .map((n) => n.getBBox().x - 2); + tt.select("rect").attrs((b, i) => ({ x: b.x + start[i] - 1, width: b.width - start[i] + 2 })); + // Now compute heights + let hm = d3.max(tt.data().map((b) => b.height)), + hh = (y.invert(0) - y.invert(hm - 1)) / 2, + stack = []; + cy.map((d) => d[1]).forEach(function (h, i) { + let n = 1; + let overlap = (s) => h / n - s.h / s.n <= hh * (s.n + n); + let l = stack.length; + while (l && overlap(stack[--l])) { + let s = stack.pop(); + h += s.h; + n += s.n; + } + stack.push({ h: h, n: n }); + }); + let ch = d3.merge( + stack.map((s, i) => { + let h = s.h / s.n - (s.n - 1) * hh; + return d3.range(s.n).map((k) => h + k * 2 * hh); + }), + ); + tt.attr("transform", (_, i) => "translate(0," + (y(ch[i]) + 5) + ")"); + dB.attr("y", y(ch[ch.length - 1] + 2 * hh) + 1); } else { - let d = 30 * W0 / gr.node().getBoundingClientRect().width, - sl= range_to_slice([-1,1],s=>m[0]+d*s); - let ind = cs - .map(c => - sl(baseline.fn(c.l)) - .map(p => Math.hypot(x(p[0])-m[0], y(p[1]+getOffset(c.p))-m[1])) - .reduce((a,b)=>Math.min(a,b), d) - ) - .reduce((a,b,i) => b m[0] + d * s); + let ind = cs + .map((c) => + sl(baseline.fn(c.l)) + .map((p) => Math.hypot(x(p[0]) - m[0], y(p[1] + getOffset(c.p)) - m[1])) + .reduce((a, b) => Math.min(a, b), d), + ) + .reduce((a, b, i) => (b < a[1] ? [i, b] : a), [-1, d])[0]; + pathHL(ind === -1 ? false : cs[ind], m, imm); } + }; +function stopInspect() { + gr.selectAll(".inspector").remove(); } -function stopInspect() { gr.selectAll(".inspector").remove(); } gr.append("rect") - .attrs({x:pad.l,y:pad.t,width:W,height:H,opacity:0}) - .on("mousemove", graphInteract()) - .on("mouseout", ()=>interactInspect?stopInspect():pathHL(false)) - .on("click", graphInteract(true)); + .attrs({ x: pad.l, y: pad.t, width: W, height: H, opacity: 0 }) + .on("mousemove", graphInteract()) + .on("mouseout", () => (interactInspect ? stopInspect() : pathHL(false))) + .on("click", graphInteract(true)); doc.select("#inspector").on("click", function () { - clearLabels(); - stopInspect(); - d3.select(this).classed("selected", interactInspect = !interactInspect); + clearLabels(); + stopInspect(); + d3.select(this).classed("selected", (interactInspect = !interactInspect)); }); doc.select("#expandTools").on("click", function () { - let t=doc.select(".tools"), cl="collapseTools", v=!t.classed(cl); - [t,doc.select(".targets")].forEach(s=>s.classed(cl, v)); + let t = doc.select(".tools"), + cl = "collapseTools", + v = !t.classed(cl); + [t, doc.select(".targets")].forEach((s) => s.classed(cl, v)); }); -d3.selectAll(".helptip").on("click", function() { - let e = d3.select(this); - e.classed("active", !e.classed("active")); +d3.selectAll(".helptip").on("click", function () { + let e = d3.select(this); + e.classed("active", !e.classed("active")); }); // Copy URL button functionality function copyUrlInit() { - let copyUrlButton = document.querySelector("button#copy-url"); + let copyUrlButton = document.querySelector("button#copy-url"); - copyUrlButton.addEventListener("click", function(e) { - let urlHost = document.createElement('input'), - currentUrl = targetWindow.location.href; + copyUrlButton.addEventListener("click", function (e) { + let urlHost = document.createElement("input"), + currentUrl = targetWindow.location.href; - urlHost.setAttribute("style","position: fixed; opacity: 0.0;"); - urlHost.value = currentUrl; - document.body.appendChild(urlHost); + urlHost.setAttribute("style", "position: fixed; opacity: 0.0;"); + urlHost.value = currentUrl; + document.body.appendChild(urlHost); - urlHost.select(); - document.execCommand('copy'); - document.body.removeChild(urlHost); + urlHost.select(); + document.execCommand("copy"); + document.body.removeChild(urlHost); - e.stopPropagation(); + e.stopPropagation(); - copyUrlButton.classList.add("clicked"); - setTimeout(function() { - copyUrlButton.classList.remove("clicked"); - }, 600); - - // Analytics event - if (analyticsEnabled) { pushEventTag("clicked_copyUrl", targetWindow); } - }); + copyUrlButton.classList.add("clicked"); + setTimeout(function () { + copyUrlButton.classList.remove("clicked"); + }, 600); + + // Analytics event + if (analyticsEnabled) { + pushEventTag("clicked_copyUrl", targetWindow); + } + }); } copyUrlInit(); // Theme Chooser function themeChooser(command) { - let docBody = document.querySelector("body"), - themeButton = document.querySelector("button#theme"), - themeCurrent = themeButton.getAttribute("current-theme"); - - // If a change event, make changes to state - if (command === "change") { - if (themeCurrent === "theme-dark") { - localStorage.setItem("theme-pref", "theme-contrast"); - } else if (themeCurrent === "theme-contrast") { - localStorage.setItem("theme-pref", "theme-default"); - } else { - localStorage.setItem("theme-pref", "theme-dark"); - } - } - - let themePref = localStorage.getItem("theme-pref"); - - // Apply state - if (themePref === "theme-dark") { - docBody.classList.remove("theme-default", "theme-contrast"); - docBody.classList.add("theme-dark"); - themeButton.textContent = "contrast mode"; - - } else if (themePref === "theme-contrast") { - docBody.classList.remove("theme-default", "theme-dark"); - docBody.classList.add("theme-contrast"); - themeButton.textContent = "default mode"; - + let docBody = document.querySelector("body"), + themeButton = document.querySelector("button#theme"), + themeCurrent = themeButton.getAttribute("current-theme"); + + // If a change event, make changes to state + if (command === "change") { + if (themeCurrent === "theme-dark") { + localStorage.setItem("theme-pref", "theme-contrast"); + } else if (themeCurrent === "theme-contrast") { + localStorage.setItem("theme-pref", "theme-default"); } else { - docBody.classList.remove("theme-dark", "theme-contrast"); - docBody.classList.add("theme-default"); - themeButton.textContent = "dark mode"; - } - - themeButton.setAttribute("current-theme", themePref); -} -if ( themingEnabled ) { - let themeButton = document.createElement("button"), - miscTools = document.querySelector("div.miscTools"); - - themeButton.setAttribute("id", "theme"); + localStorage.setItem("theme-pref", "theme-dark"); + } + } + + let themePref = localStorage.getItem("theme-pref"); + + // Apply state + if (themePref === "theme-dark") { + docBody.classList.remove("theme-default", "theme-contrast"); + docBody.classList.add("theme-dark"); + themeButton.textContent = "contrast mode"; + } else if (themePref === "theme-contrast") { + docBody.classList.remove("theme-default", "theme-dark"); + docBody.classList.add("theme-contrast"); + themeButton.textContent = "default mode"; + } else { + docBody.classList.remove("theme-dark", "theme-contrast"); + docBody.classList.add("theme-default"); themeButton.textContent = "dark mode"; - themeButton.setAttribute("current-theme", "theme-default"); - miscTools.append(themeButton); - - themeChooser(); + } + + themeButton.setAttribute("current-theme", themePref); +} +if (themingEnabled) { + let themeButton = document.createElement("button"), + miscTools = document.querySelector("div.miscTools"); + + themeButton.setAttribute("id", "theme"); + themeButton.textContent = "dark mode"; + themeButton.setAttribute("current-theme", "theme-default"); + miscTools.append(themeButton); + + themeChooser(); } // Map faux download button function mapDownloadFaux() { - let downloadButton = document.querySelector("button#download"), - downloadFaux = document.querySelector("button#download-faux"); - - downloadFaux.addEventListener("click", function() { - downloadButton.click(); - }); + let downloadButton = document.querySelector("button#download"), + downloadFaux = document.querySelector("button#download-faux"); + + downloadFaux.addEventListener("click", function () { + downloadButton.click(); + }); } mapDownloadFaux(); // Set focused scroll list function setFocusedList(selectedList) { - let listsContainer = document.querySelector("div.select"); + let listsContainer = document.querySelector("div.select"); - listsContainer.setAttribute("data-selected", selectedList) + listsContainer.setAttribute("data-selected", selectedList); } function focusedListClicks() { - let listClickTragets = document.querySelectorAll("*[data-list=\"brands\"], *[data-list=\"models\"]"); + let listClickTragets = document.querySelectorAll('*[data-list="brands"], *[data-list="models"]'); - listClickTragets.forEach((clickedTarget) => { - clickedTarget.addEventListener("click", () => { - let selectedList = clickedTarget.getAttribute("data-list") - setFocusedList(selectedList); - window.hideExtraPanel && window.hideExtraPanel(selectedList); - }); + listClickTragets.forEach((clickedTarget) => { + clickedTarget.addEventListener("click", () => { + let selectedList = clickedTarget.getAttribute("data-list"); + setFocusedList(selectedList); + window.hideExtraPanel && window.hideExtraPanel(selectedList); }); + }); - let brandsList = document.querySelector("div.scroll#brands"); - - brandsList.addEventListener("click", function(e) { - let clickedElem = e.target, - clickedElemIsBrand = clickedElem.matches("div.scroll#brands div"); - - if (clickedElemIsBrand) { - setFocusedList("models"); - e.stopPropagation(); - } - }); + let brandsList = document.querySelector("div.scroll#brands"); + + brandsList.addEventListener("click", function (e) { + let clickedElem = e.target, + clickedElemIsBrand = clickedElem.matches("div.scroll#brands div"); + if (clickedElemIsBrand) { + setFocusedList("models"); + e.stopPropagation(); + } + }); } focusedListClicks(); function focusedListSwipes() { - let horizontalSwipeTarget = document.querySelector("div.scroll-container"), - listsContainer = document.querySelector("div.select"), - swipableList = document.querySelector("div.scrollOuter[data-list=\"models\"]"); - touchDelta = 0; - - horizontalSwipeTarget.addEventListener("touchstart", function(e) { - selectedList = listsContainer.getAttribute("data-selected"); - touchStart = e.targetTouches[0].screenX; - - horizontalSwipeTarget.addEventListener("touchmove", function(e) { - touchNow = e.targetTouches[0].screenX; - touchDelta = touchNow - touchStart, - touchDeltaNegative = 0 - touchDelta; - - if ( selectedList === "models" && touchDelta > 0 && touchDelta < 100 ) { - swipableList.setAttribute("style","right: "+ touchDeltaNegative +"px;") - } - - if ( selectedList === "brands" && touchDelta < 0 && touchDelta > -100 ) { - swipableList.setAttribute("style","right: "+ touchDeltaNegative +"px;") - } - }); + let horizontalSwipeTarget = document.querySelector("div.scroll-container"), + listsContainer = document.querySelector("div.select"), + swipableList = document.querySelector('div.scrollOuter[data-list="models"]'); + touchDelta = 0; + + horizontalSwipeTarget.addEventListener("touchstart", function (e) { + selectedList = listsContainer.getAttribute("data-selected"); + touchStart = e.targetTouches[0].screenX; + + horizontalSwipeTarget.addEventListener("touchmove", function (e) { + touchNow = e.targetTouches[0].screenX; + (touchDelta = touchNow - touchStart), (touchDeltaNegative = 0 - touchDelta); + + if (selectedList === "models" && touchDelta > 0 && touchDelta < 100) { + swipableList.setAttribute("style", "right: " + touchDeltaNegative + "px;"); + } + + if (selectedList === "brands" && touchDelta < 0 && touchDelta > -100) { + swipableList.setAttribute("style", "right: " + touchDeltaNegative + "px;"); + } }); + }); - horizontalSwipeTarget.addEventListener("touchend", function(e) { - if ( touchDelta > 49 ) { - listsContainer.setAttribute("data-selected","brands"); - } + horizontalSwipeTarget.addEventListener("touchend", function (e) { + if (touchDelta > 49) { + listsContainer.setAttribute("data-selected", "brands"); + } - if ( touchDelta < -50 ) { - listsContainer.setAttribute("data-selected","models"); - } - - swipableList.setAttribute("style","") - touchStart = 0; - touchNow = 0; - touchDelta = 0; - - //horizontalSwipeTarget.removeEventListener("touchmove"); - }); + if (touchDelta < -50) { + listsContainer.setAttribute("data-selected", "models"); + } + + swipableList.setAttribute("style", ""); + touchStart = 0; + touchNow = 0; + touchDelta = 0; + + //horizontalSwipeTarget.removeEventListener("touchmove"); + }); } focusedListSwipes(); // Scroll list to active phone on init function scrollToActive() { - try { - let phoneList = document.querySelector('div.scroll#phones'), - firstActivePhone = document.querySelector('div.phone-item[style*=border]'), - offset = firstActivePhone.offsetTop - 26; + try { + let phoneList = document.querySelector("div.scroll#phones"), + firstActivePhone = document.querySelector("div.phone-item[style*=border]"), + offset = firstActivePhone.offsetTop - 26; - phoneList.scrollTop = offset; - } - catch {} + phoneList.scrollTop = offset; + } catch {} } // Set focused panel function setFocusedPanel() { - let panelsContainer = document.querySelector("main.main"), - primaryPanel = document.querySelector(".parts-primary"), - secondaryPanel = document.querySelector(".parts-secondary"), - phonesList = document.querySelector("div#phones"), - graphBox = document.querySelector("div.graph-sizer"), - mobileHelper = document.querySelector("tr.mobile-helper"); - - panelsContainer.setAttribute("data-focused-panel","secondary"); - - mobileHelper.addEventListener("click", function() { - panelsContainer.setAttribute("data-focused-panel","secondary"); - }); + let panelsContainer = document.querySelector("main.main"), + primaryPanel = document.querySelector(".parts-primary"), + secondaryPanel = document.querySelector(".parts-secondary"), + phonesList = document.querySelector("div#phones"), + graphBox = document.querySelector("div.graph-sizer"), + mobileHelper = document.querySelector("tr.mobile-helper"); - secondaryPanel.addEventListener("click", function() { - panelsContainer.setAttribute("data-focused-panel","secondary"); - }); - - graphBox.addEventListener("click", function() { - let previousState = panelsContainer.getAttribute("data-focused-panel"); - - if ( previousState === "primary") { - panelsContainer.setAttribute("data-focused-panel","secondary"); - } else if ( previousState === "secondary" ) { - panelsContainer.setAttribute("data-focused-panel","primary"); + panelsContainer.setAttribute("data-focused-panel", "secondary"); + + mobileHelper.addEventListener("click", function () { + panelsContainer.setAttribute("data-focused-panel", "secondary"); + }); + + secondaryPanel.addEventListener("click", function () { + panelsContainer.setAttribute("data-focused-panel", "secondary"); + }); + + graphBox.addEventListener("click", function () { + let previousState = panelsContainer.getAttribute("data-focused-panel"); + + if (previousState === "primary") { + panelsContainer.setAttribute("data-focused-panel", "secondary"); + } else if (previousState === "secondary") { + panelsContainer.setAttribute("data-focused-panel", "primary"); + } + }); + + // Touch events + let verticalSwipeTargets = document.querySelectorAll("div.selector-tabs, input.search"); + + verticalSwipeTargets.forEach(function (target) { + target.addEventListener("touchstart", function (e) { + focusedPanel = document.querySelector("main.main").getAttribute("data-focused-panel"); + + touchStart = e.targetTouches[0].screenY; + + target.addEventListener("touchmove", function (e) { + touchNow = e.targetTouches[0].screenY; + touchDelta = touchNow - touchStart; + + if (focusedPanel === "secondary" && touchDelta > 0 && touchDelta < 200) { + secondaryPanel.setAttribute("style", "top: " + touchDelta + "px;"); + } else if (focusedPanel === "primary" && touchDelta < 0 && touchDelta > -200) { + secondaryPanel.setAttribute("style", "top: " + touchDelta + "px;"); } + }); }); - - // Touch events - let verticalSwipeTargets = document.querySelectorAll("div.selector-tabs, input.search"); - - verticalSwipeTargets.forEach(function(target) { - target.addEventListener("touchstart", function(e) { - focusedPanel = document.querySelector("main.main").getAttribute("data-focused-panel"); - - touchStart = e.targetTouches[0].screenY; - - target.addEventListener("touchmove", function(e) { - touchNow = e.targetTouches[0].screenY; - touchDelta = touchNow - touchStart; - - if ( focusedPanel === "secondary" && touchDelta > 0 && touchDelta < 200) { - secondaryPanel.setAttribute("style", "top: " + touchDelta + "px;") - } else if ( focusedPanel === "primary" && touchDelta < 0 && touchDelta > -200) { - secondaryPanel.setAttribute("style", "top: " + touchDelta + "px;") - } - }); - }); - target.addEventListener("touchend", function(e) { - if ( touchDelta > 49 ) { - panelsContainer.setAttribute("data-focused-panel","primary"); - } + target.addEventListener("touchend", function (e) { + if (touchDelta > 49) { + panelsContainer.setAttribute("data-focused-panel", "primary"); + } - if ( touchDelta < -50 ) { - panelsContainer.setAttribute("data-focused-panel","secondary"); - } + if (touchDelta < -50) { + panelsContainer.setAttribute("data-focused-panel", "secondary"); + } - secondaryPanel.setAttribute("style", "") - touchStart = 0; - touchNow = 0; - touchDelta = 0; - }); - - target.addEventListener("wheel", function(e) { - let wheelDelta = e.deltaY; + secondaryPanel.setAttribute("style", ""); + touchStart = 0; + touchNow = 0; + touchDelta = 0; + }); - if (wheelDelta < -5) { - panelsContainer.setAttribute("data-focused-panel","primary"); - } + target.addEventListener("wheel", function (e) { + let wheelDelta = e.deltaY; - if (wheelDelta > 5) { - panelsContainer.setAttribute("data-focused-panel","secondary"); - } - }); + if (wheelDelta < -5) { + panelsContainer.setAttribute("data-focused-panel", "primary"); + } + + if (wheelDelta > 5) { + panelsContainer.setAttribute("data-focused-panel", "secondary"); + } }); + }); } setFocusedPanel(); // Blur focus from inputs on submit function blurFocus() { - let inputFields = document.querySelectorAll("input"), - body = document.querySelector("body"); - - inputFields.forEach(function(field) { - field.addEventListener("keyup", function(e) { - if (e.keyCode === 13) { - field.blur(); - } - }); - - field.addEventListener("focus", function() { - body.setAttribute("data-input-state","focus"); - }); - - field.addEventListener("blur", function() { - body.setAttribute("data-input-state","blur"); - }); + let inputFields = document.querySelectorAll("input"), + body = document.querySelector("body"); + + inputFields.forEach(function (field) { + field.addEventListener("keyup", function (e) { + if (e.keyCode === 13) { + field.blur(); + } }); + + field.addEventListener("focus", function () { + body.setAttribute("data-input-state", "focus"); + }); + + field.addEventListener("blur", function () { + body.setAttribute("data-input-state", "blur"); + }); + }); } blurFocus(); // Add extra feature function addExtra() { - let extraButton = document.querySelector("div.select > div.selector-tabs > button.extra"); - // Disable functions by config - if (!extraEnabled) { - extraButton.remove(); - return; - } - if (!extraUploadEnabled) { - document.querySelector("div.extra-panel > div.extra-upload").style["display"] = "none"; - } - if (!extraEQEnabled) { - document.querySelector("div.extra-panel > div.extra-eq").style["display"] = "none"; - } - if (!extraToneGeneratorEnabled) { - document.querySelector("div.extra-panel > div.extra-tone-generator").style["display"] = "none"; - } - // Show and hide extra panel - window.showExtraPanel = () => { - document.querySelector("div.select > div.selector-panel").style["display"] = "none"; - document.querySelector("div.select > div.extra-panel").style["display"] = "flex"; - document.querySelector("div.select").setAttribute("data-selected", "extra"); - if (analyticsEnabled) { pushEventTag("clicked_equalizerTab", targetWindow); } - }; - window.hideExtraPanel = (selectedList) => { - document.querySelector("div.select > div.selector-panel").style["display"] = "flex"; - document.querySelector("div.select > div.extra-panel").style["display"] = "none"; - document.querySelector("div.select").setAttribute("data-selected", selectedList); - }; - extraButton.addEventListener("click", showExtraPanel); - // Upload function - let uploadType = null; - let fileFR = document.querySelector("#file-fr"); - document.querySelector("div.extra-upload > button.upload-fr").addEventListener("click", () => { - uploadType = "fr"; - fileFR.click(); + let extraButton = document.querySelector("div.select > div.selector-tabs > button.extra"); + // Disable functions by config + if (!extraEnabled) { + extraButton.remove(); + return; + } + if (!extraUploadEnabled) { + document.querySelector("div.extra-panel > div.extra-upload").style["display"] = "none"; + } + if (!extraEQEnabled) { + document.querySelector("div.extra-panel > div.extra-eq").style["display"] = "none"; + } + if (!extraToneGeneratorEnabled) { + document.querySelector("div.extra-panel > div.extra-tone-generator").style["display"] = "none"; + } + // Show and hide extra panel + window.showExtraPanel = () => { + document.querySelector("div.select > div.selector-panel").style["display"] = "none"; + document.querySelector("div.select > div.extra-panel").style["display"] = "flex"; + document.querySelector("div.select").setAttribute("data-selected", "extra"); + if (analyticsEnabled) { + pushEventTag("clicked_equalizerTab", targetWindow); + } + }; + window.hideExtraPanel = (selectedList) => { + document.querySelector("div.select > div.selector-panel").style["display"] = "flex"; + document.querySelector("div.select > div.extra-panel").style["display"] = "none"; + document.querySelector("div.select").setAttribute("data-selected", selectedList); + }; + extraButton.addEventListener("click", showExtraPanel); + // Upload function + let uploadType = null; + let fileFR = document.querySelector("#file-fr"); + document.querySelector("div.extra-upload > button.upload-fr").addEventListener("click", () => { + uploadType = "fr"; + fileFR.click(); + }); + document + .querySelector("div.extra-upload > button.upload-target") + .addEventListener("click", () => { + uploadType = "target"; + fileFR.click(); }); - document.querySelector("div.extra-upload > button.upload-target").addEventListener("click", () => { - uploadType = "target"; - fileFR.click(); - }); - let addOrUpdatePhone = (brand, phone, ch) => { - let phoneObj = asPhoneObj(brand, phone); - phoneObj.rawChannels = ch; - phoneObj.isDynamic = true; - let phoneObjs = brand.phoneObjs; - let oldPhoneObj = phoneObjs.filter(p => p.phone == phone.name)[0] - if (oldPhoneObj) { - oldPhoneObj.active && removePhone(oldPhoneObj); - phoneObj.id = oldPhoneObj.id; - phoneObjs[phoneObjs.indexOf(oldPhoneObj)] = phoneObj; - allPhones[allPhones.indexOf(oldPhoneObj)] = phoneObj; - } else { - brand.phones.push(phone); - phoneObjs.push(phoneObj); - allPhones.push(phoneObj); - } - updatePhoneSelect(); - return phoneObj; - }; - fileFR.addEventListener("change", (e) => { - let file = e.target.files[0]; - if (!file) { - return; + let addOrUpdatePhone = (brand, phone, ch) => { + let phoneObj = asPhoneObj(brand, phone); + phoneObj.rawChannels = ch; + phoneObj.isDynamic = true; + let phoneObjs = brand.phoneObjs; + let oldPhoneObj = phoneObjs.filter((p) => p.phone == phone.name)[0]; + if (oldPhoneObj) { + oldPhoneObj.active && removePhone(oldPhoneObj); + phoneObj.id = oldPhoneObj.id; + phoneObjs[phoneObjs.indexOf(oldPhoneObj)] = phoneObj; + allPhones[allPhones.indexOf(oldPhoneObj)] = phoneObj; + } else { + brand.phones.push(phone); + phoneObjs.push(phoneObj); + allPhones.push(phoneObj); + } + updatePhoneSelect(); + return phoneObj; + }; + fileFR.addEventListener("change", (e) => { + let file = e.target.files[0]; + if (!file) { + return; + } + let reader = new FileReader(); + reader.onload = (e) => { + let name = file.name.replace(/\.[^\.]+$/, ""); + let phone = { name: name }; + let ch = [tsvParse(e.target.result)]; + if (ch[0].length < 128) { + alert("Parse frequence response file failed: invalid format."); + return; + } + ch[0] = Equalizer.interp(f_values, ch[0]); + if (uploadType === "fr") { + name.match(/ R$/) && ch.splice(0, 0, null); + let phoneObj = addOrUpdatePhone(brandMap.Uploaded, phone, ch); + showPhone(phoneObj, false); + } else if (uploadType === "target") { + let fullName = name + (name.match(/ Target$/i) ? "" : " Target"); + let existsTargets = targets + .reduce((a, b) => a.concat(b.files), []) + .map((f) => (f += " Target")); + if (existsTargets.indexOf(fullName) >= 0) { + alert("This target already exists on this tool, please select it instead of upload."); + return; } - let reader = new FileReader(); - reader.onload = (e) => { - let name = file.name.replace(/\.[^\.]+$/, ""); - let phone = { name: name }; - let ch = [tsvParse(e.target.result)]; - if (ch[0].length < 128) { - alert("Parse frequence response file failed: invalid format."); - return; - } - ch[0] = Equalizer.interp(f_values, ch[0]); - if (uploadType === "fr") { - name.match(/ R$/) && ch.splice(0, 0, null); - let phoneObj = addOrUpdatePhone(brandMap.Uploaded, phone, ch); - showPhone(phoneObj, false); - } else if (uploadType === "target") { - let fullName = name + (name.match(/ Target$/i) ? "" : " Target"); - let existsTargets = targets.reduce((a, b) => a.concat(b.files), []).map(f => f += " Target"); - if (existsTargets.indexOf(fullName) >= 0) { - alert("This target already exists on this tool, please select it instead of upload."); - return; - } - let phoneObj = { - isTarget: true, - brand: brandTarget, - dispName: name, - phone: name, - fullName: fullName, - fileName: fullName, - rawChannels: ch, - isDynamic: true, - id: -brandTarget.phoneObjs.length - }; - showPhone(phoneObj, true); - } + let phoneObj = { + isTarget: true, + brand: brandTarget, + dispName: name, + phone: name, + fullName: fullName, + fileName: fullName, + rawChannels: ch, + isDynamic: true, + id: -brandTarget.phoneObjs.length, }; - reader.readAsText(file); - }); - // EQ Function - let eqPhoneSelect = document.querySelector("div.extra-eq select[name='phone']"); - let filtersContainer = document.querySelector("div.extra-eq > div.filters"); - let fileFiltersImport = document.querySelector("#file-filters-import"); - let filterEnabledInput, filterTypeSelect, - filterFreqInput, filterQInput, filterGainInput; - let eqBands = extraEQBands; - let updateFilterElements = () => { - let node = filtersContainer.querySelector("div.filter"); - while (filtersContainer.childElementCount < eqBands) { - let clone = node.cloneNode(true); - clone.querySelector("input[name='enabled']").value = "true"; - clone.querySelector("select[name='type']").value = "PK"; - clone.querySelector("input[name='freq']").value = "0"; - clone.querySelector("input[name='q']").value = "0"; - clone.querySelector("input[name='gain']").value = "0"; - filtersContainer.appendChild(clone); - } - while (filtersContainer.childElementCount > eqBands) { - filtersContainer.children[filtersContainer.childElementCount-1].remove(); - } - filterEnabledInput = filtersContainer.querySelectorAll("input[name='enabled']"); - filterTypeSelect = filtersContainer.querySelectorAll("select[name='type']"); - filterFreqInput = filtersContainer.querySelectorAll("input[name='freq']"); - filterQInput = filtersContainer.querySelectorAll("input[name='q']"); - filterGainInput = filtersContainer.querySelectorAll("input[name='gain']"); - filtersContainer.querySelectorAll("input,select").forEach(el => { - el.removeEventListener("input", applyEQ); - el.addEventListener("input", applyEQ); - }); - }; - let elemToFilters = (includeAll) => { - // Collect filters from ui - let filters = []; - for (let i = 0; i < eqBands; ++i) { - let disabled = !filterEnabledInput[i].checked; - let type = filterTypeSelect[i].value; - let freq = parseInt(filterFreqInput[i].value) || 0; - let q = parseFloat(filterQInput[i].value) || 0; - let gain = parseFloat(filterGainInput[i].value) || 0; - if (!includeAll && (disabled || !type || !freq || !q || !gain)) { - continue; - } - filters.push({ disabled, type, freq, q, gain }); - } - return filters; - }; - let filtersToElem = (filters) => { - // Set filters to ui - let filtersCopy = filters.map(f => f); - while (filtersCopy.length < eqBands) { - filtersCopy.push({ type: "PK", freq: 0, q: 0, gain: 0 }); - } - if (filtersCopy.length > eqBands) { - eqBands = Math.min(filtersCopy.length, extraEQBandsMax); - filtersCopy = filtersCopy.slice(0, eqBands); - updateFilterElements(); - } - filtersCopy.forEach((f, i) => { - filterEnabledInput[i].checked = !f.disabled; - filterTypeSelect[i].value = f.type; - filterFreqInput[i].value = f.freq; - filterQInput[i].value = f.q; - filterGainInput[i].value = f.gain; - }); - }; - let applyEQHandle = null; - let applyEQExec = () => { - // Create and show phone with eq applied - let activeElem = document.activeElement; - let phoneSelected = eqPhoneSelect.value; - let filters = elemToFilters(); - if (filters.length && !phoneSelected) { - let firstPhone = eqPhoneSelect.querySelectorAll("option")[1]; - if (firstPhone) { - phoneSelected = eqPhoneSelect.value = firstPhone.value; - } - } - let phoneObj = phoneSelected && activePhones.filter( - p => p.fullName == phoneSelected)[0]; - if (!phoneObj || (!filters.length && !phoneObj.eq)) { - return; // Allow empty filters if eq is applied before - } - let phoneEQ = { name: phoneObj.phone + " EQ" }; - let phoneObjEQ = addOrUpdatePhone(phoneObj.brand, phoneEQ, - phoneObj.rawChannels.map(c => c ? Equalizer.apply(c, filters) : null)); - phoneObj.eq = phoneObjEQ; - phoneObjEQ.eqParent = phoneObj; - showPhone(phoneObjEQ, false); - activeElem.focus(); + showPhone(phoneObj, true); + } }; - let applyEQ = () => { - clearTimeout(applyEQHandle); - applyEQHandle = setTimeout(applyEQExec, 100); - }; - window.updateEQPhoneSelect = () => { - let oldValue = eqPhoneSelect.value; - let optionValues = activePhones.filter(p => - !p.isTarget && !p.fullName.match(/ EQ$/)).map(p => p.fullName); - Array.from(eqPhoneSelect.children).slice(1).forEach(c => eqPhoneSelect.removeChild(c)); - optionValues.forEach(value => { - let optionElem = document.createElement("option"); - optionElem.setAttribute("value", value); - optionElem.innerText = value; - eqPhoneSelect.appendChild(optionElem); - }); - eqPhoneSelect.value = (optionValues.indexOf(oldValue) >= 0) ? oldValue : ""; - }; - updateFilterElements(); - eqPhoneSelect.addEventListener("input", applyEQ); - // Add new filter - document.querySelector("div.extra-eq button.add-filter").addEventListener("click", () => { - eqBands = Math.min(eqBands + 1, extraEQBandsMax); - updateFilterElements(); + reader.readAsText(file); + }); + // EQ Function + let eqPhoneSelect = document.querySelector("div.extra-eq select[name='phone']"); + let filtersContainer = document.querySelector("div.extra-eq > div.filters"); + let fileFiltersImport = document.querySelector("#file-filters-import"); + let filterEnabledInput, filterTypeSelect, filterFreqInput, filterQInput, filterGainInput; + let eqBands = extraEQBands; + let updateFilterElements = () => { + let node = filtersContainer.querySelector("div.filter"); + while (filtersContainer.childElementCount < eqBands) { + let clone = node.cloneNode(true); + clone.querySelector("input[name='enabled']").value = "true"; + clone.querySelector("select[name='type']").value = "PK"; + clone.querySelector("input[name='freq']").value = "0"; + clone.querySelector("input[name='q']").value = "0"; + clone.querySelector("input[name='gain']").value = "0"; + filtersContainer.appendChild(clone); + } + while (filtersContainer.childElementCount > eqBands) { + filtersContainer.children[filtersContainer.childElementCount - 1].remove(); + } + filterEnabledInput = filtersContainer.querySelectorAll("input[name='enabled']"); + filterTypeSelect = filtersContainer.querySelectorAll("select[name='type']"); + filterFreqInput = filtersContainer.querySelectorAll("input[name='freq']"); + filterQInput = filtersContainer.querySelectorAll("input[name='q']"); + filterGainInput = filtersContainer.querySelectorAll("input[name='gain']"); + filtersContainer.querySelectorAll("input,select").forEach((el) => { + el.removeEventListener("input", applyEQ); + el.addEventListener("input", applyEQ); }); - // Remove last filter - document.querySelector("div.extra-eq button.remove-filter").addEventListener("click", () => { - eqBands = Math.max(eqBands - 1, 1); - updateFilterElements(); - applyEQ(); // May removed effective filter + }; + let elemToFilters = (includeAll) => { + // Collect filters from ui + let filters = []; + for (let i = 0; i < eqBands; ++i) { + let disabled = !filterEnabledInput[i].checked; + let type = filterTypeSelect[i].value; + let freq = parseInt(filterFreqInput[i].value) || 0; + let q = parseFloat(filterQInput[i].value) || 0; + let gain = parseFloat(filterGainInput[i].value) || 0; + if (!includeAll && (disabled || !type || !freq || !q || !gain)) { + continue; + } + filters.push({ disabled, type, freq, q, gain }); + } + return filters; + }; + let filtersToElem = (filters) => { + // Set filters to ui + let filtersCopy = filters.map((f) => f); + while (filtersCopy.length < eqBands) { + filtersCopy.push({ type: "PK", freq: 0, q: 0, gain: 0 }); + } + if (filtersCopy.length > eqBands) { + eqBands = Math.min(filtersCopy.length, extraEQBandsMax); + filtersCopy = filtersCopy.slice(0, eqBands); + updateFilterElements(); + } + filtersCopy.forEach((f, i) => { + filterEnabledInput[i].checked = !f.disabled; + filterTypeSelect[i].value = f.type; + filterFreqInput[i].value = f.freq; + filterQInput[i].value = f.q; + filterGainInput[i].value = f.gain; }); - // Sort filters by frequency - document.querySelector("div.extra-eq button.sort-filters").addEventListener("click", () => { - filtersToElem(elemToFilters(true).sort((a, b) => - (a.freq || Infinity) - (b.freq || Infinity))); - }); - // Import filters - document.querySelector("div.extra-eq button.import-filters").addEventListener("click", () => { - fileFiltersImport.click(); - }); - fileFiltersImport.addEventListener("change", (e) => { - // Import filters callback - let file = e.target.files[0]; - if (!file) { - return; - } - let reader = new FileReader(); - reader.onload = (e) => { - let settings = e.target.result; - let filters = settings.split("\n").map(l => { - let r = l.match(/Filter\s*\d+:\s*(\S+)\s*(\S+)\s*Fc\s*(\S+)\s*Hz\s*Gain\s*(\S+)\s*dB(\s*Q\s*(\S+))?/); - if (!r) { return undefined; } - let disabled = (r[1] !== "ON"); - let type = r[2]; - let freq = parseInt(r[3]) || 0; - let gain = parseFloat(r[4]) || 0; - let q = parseFloat(r[6]) || 0; - if (type === "LS" || type === "HS") { - type += "Q"; - q = q || 0.707; - } else if (type === "LSC" || type === "HSC") { - // Equalizer APO use LSC/HSC instead of LSQ/HSQ - type = type.substr(0, 2) + "Q"; - } - return { disabled, type, freq, q, gain }; - }).filter(f => f); - while (filters.length > 0) { - // Remove empty tail filters - let lastFilter = filters[filters.length-1]; - if (!lastFilter.freq && !lastFilter.q && !lastFilter.gain) { - filters.pop(); - } else { - break; - } - } - if (filters.length > 0) { - filtersToElem(filters); - applyEQ(); - } else { - alert("Parse filters file failed: no filter found."); - } - }; - reader.readAsText(file); + }; + let applyEQHandle = null; + let applyEQExec = () => { + // Create and show phone with eq applied + let activeElem = document.activeElement; + let phoneSelected = eqPhoneSelect.value; + let filters = elemToFilters(); + if (filters.length && !phoneSelected) { + let firstPhone = eqPhoneSelect.querySelectorAll("option")[1]; + if (firstPhone) { + phoneSelected = eqPhoneSelect.value = firstPhone.value; + } + } + let phoneObj = phoneSelected && activePhones.filter((p) => p.fullName == phoneSelected)[0]; + if (!phoneObj || (!filters.length && !phoneObj.eq)) { + return; // Allow empty filters if eq is applied before + } + let phoneEQ = { name: phoneObj.phone + " EQ" }; + let phoneObjEQ = addOrUpdatePhone( + phoneObj.brand, + phoneEQ, + phoneObj.rawChannels.map((c) => (c ? Equalizer.apply(c, filters) : null)), + ); + phoneObj.eq = phoneObjEQ; + phoneObjEQ.eqParent = phoneObj; + showPhone(phoneObjEQ, false); + activeElem.focus(); + }; + let applyEQ = () => { + clearTimeout(applyEQHandle); + applyEQHandle = setTimeout(applyEQExec, 100); + }; + window.updateEQPhoneSelect = () => { + let oldValue = eqPhoneSelect.value; + let optionValues = activePhones + .filter((p) => !p.isTarget && !p.fullName.match(/ EQ$/)) + .map((p) => p.fullName); + Array.from(eqPhoneSelect.children) + .slice(1) + .forEach((c) => eqPhoneSelect.removeChild(c)); + optionValues.forEach((value) => { + let optionElem = document.createElement("option"); + optionElem.setAttribute("value", value); + optionElem.innerText = value; + eqPhoneSelect.appendChild(optionElem); }); - // Export filters - document.querySelector("div.extra-eq button.export-filters").addEventListener("click", () => { - let phoneSelected = eqPhoneSelect.value; - let phoneObj = phoneSelected && activePhones.filter( - p => p.fullName == phoneSelected && p.eq)[0]; - let filters = elemToFilters(true); - if (!phoneObj || !filters.length) { - alert("Please select model and add atleast one filter before export."); - return; + eqPhoneSelect.value = optionValues.indexOf(oldValue) >= 0 ? oldValue : ""; + }; + updateFilterElements(); + eqPhoneSelect.addEventListener("input", applyEQ); + // Add new filter + document.querySelector("div.extra-eq button.add-filter").addEventListener("click", () => { + eqBands = Math.min(eqBands + 1, extraEQBandsMax); + updateFilterElements(); + }); + // Remove last filter + document.querySelector("div.extra-eq button.remove-filter").addEventListener("click", () => { + eqBands = Math.max(eqBands - 1, 1); + updateFilterElements(); + applyEQ(); // May removed effective filter + }); + // Sort filters by frequency + document.querySelector("div.extra-eq button.sort-filters").addEventListener("click", () => { + filtersToElem(elemToFilters(true).sort((a, b) => (a.freq || Infinity) - (b.freq || Infinity))); + }); + // Import filters + document.querySelector("div.extra-eq button.import-filters").addEventListener("click", () => { + fileFiltersImport.click(); + }); + fileFiltersImport.addEventListener("change", (e) => { + // Import filters callback + let file = e.target.files[0]; + if (!file) { + return; + } + let reader = new FileReader(); + reader.onload = (e) => { + let settings = e.target.result; + let filters = settings + .split("\n") + .map((l) => { + let r = l.match( + /Filter\s*\d+:\s*(\S+)\s*(\S+)\s*Fc\s*(\S+)\s*Hz\s*Gain\s*(\S+)\s*dB(\s*Q\s*(\S+))?/, + ); + if (!r) { + return undefined; + } + let disabled = r[1] !== "ON"; + let type = r[2]; + let freq = parseInt(r[3]) || 0; + let gain = parseFloat(r[4]) || 0; + let q = parseFloat(r[6]) || 0; + if (type === "LS" || type === "HS") { + type += "Q"; + q = q || 0.707; + } else if (type === "LSC" || type === "HSC") { + // Equalizer APO use LSC/HSC instead of LSQ/HSQ + type = type.substr(0, 2) + "Q"; + } + return { disabled, type, freq, q, gain }; + }) + .filter((f) => f); + while (filters.length > 0) { + // Remove empty tail filters + let lastFilter = filters[filters.length - 1]; + if (!lastFilter.freq && !lastFilter.q && !lastFilter.gain) { + filters.pop(); + } else { + break; } - let preamp = Equalizer.calc_preamp( - phoneObj.rawChannels.filter(c => c)[0], - phoneObj.eq.rawChannels.filter(c => c)[0]); - let settings = "Preamp: " + preamp.toFixed(1) + " dB\r\n"; - filters.forEach((f, i) => { - let filterValid = f.freq != 0 && f.q != 0 && f.gain != 0 ? true : false; - - if (filterValid) { - let on = (!f.disabled && f.type && f.freq && f.gain && f.q) ? "ON" : "OFF"; - let type = f.type; - if (type === "LSQ" || type === "HSQ") { - // Equalizer APO use LSC/HSC instead of LSQ/HSQ - type = type.substr(0, 2) + "C"; - } - settings += ("Filter " + (i+1) + ": " + on + " " + type + " Fc " + - f.freq.toFixed(0) + " Hz Gain " + f.gain.toFixed(1) + " dB Q " + - f.q.toFixed(3) + "\r\n"); - } - }); - let exportElem = document.querySelector("#file-filters-export"); - exportElem.href && URL.revokeObjectURL(exportElem.href); - exportElem.href = URL.createObjectURL(new Blob([settings])); - exportElem.download = phoneObj.fullName.replace(/^Uploaded /, "") + " Filters.txt"; - exportElem.click(); - }); - // Export filters as graphic eq (for wavelet) - document.querySelector("div.extra-eq button.export-graphic-filters").addEventListener("click", () => { - let phoneSelected = eqPhoneSelect.value; - let phoneObj = phoneSelected && activePhones.filter( - p => p.fullName == phoneSelected && p.eq)[0] || { fullName: "Unnamed" }; - let filters = elemToFilters(); - if (!filters.length) { - alert("Please add atleast one filter before export."); - return; + } + if (filters.length > 0) { + filtersToElem(filters); + applyEQ(); + } else { + alert("Parse filters file failed: no filter found."); + } + }; + reader.readAsText(file); + }); + // Export filters + document.querySelector("div.extra-eq button.export-filters").addEventListener("click", () => { + let phoneSelected = eqPhoneSelect.value; + let phoneObj = + phoneSelected && activePhones.filter((p) => p.fullName == phoneSelected && p.eq)[0]; + let filters = elemToFilters(true); + if (!phoneObj || !filters.length) { + alert("Please select model and add atleast one filter before export."); + return; + } + let preamp = Equalizer.calc_preamp( + phoneObj.rawChannels.filter((c) => c)[0], + phoneObj.eq.rawChannels.filter((c) => c)[0], + ); + let settings = "Preamp: " + preamp.toFixed(1) + " dB\r\n"; + filters.forEach((f, i) => { + let filterValid = f.freq != 0 && f.q != 0 && f.gain != 0 ? true : false; + + if (filterValid) { + let on = !f.disabled && f.type && f.freq && f.gain && f.q ? "ON" : "OFF"; + let type = f.type; + if (type === "LSQ" || type === "HSQ") { + // Equalizer APO use LSC/HSC instead of LSQ/HSQ + type = type.substr(0, 2) + "C"; } - let graphicEQ = Equalizer.as_graphic_eq(filters); - let settings = "GraphicEQ: " + graphicEQ.map(([f, gain]) => - f.toFixed(0) + " " + gain.toFixed(1)).join("; "); - let exportElem = document.querySelector("#file-filters-export"); - exportElem.href && URL.revokeObjectURL(exportElem.href); - exportElem.href = URL.createObjectURL(new Blob([settings])); - exportElem.download = phoneObj.fullName.replace(/^Uploaded /, "") + " Graphic Filters.txt"; - exportElem.click(); + settings += + "Filter " + + (i + 1) + + ": " + + on + + " " + + type + + " Fc " + + f.freq.toFixed(0) + + " Hz Gain " + + f.gain.toFixed(1) + + " dB Q " + + f.q.toFixed(3) + + "\r\n"; + } }); - // Readme - document.querySelector("div.extra-eq button.readme").addEventListener("click", () => { - alert("1. If you want to AutoEQ model A to B, display A B and remove target\n" + - "2. Add/Remove bands before AutoEQ may give you a better result\n" + - "3. Curve of PK filter close to 20K is implementation dependent, avoid such filter if you're not sure how your DSP software works\n" + - "4. EQ treble require resonant peak matching and fine tune by ear, keep treble untouched if you're not sure how to do that\n" + - "5. Tone generator is useful to find actual location of peaks and dips, notice the web version may not work on some platform\n"); - }); - // AutoEQ - let autoEQFromInput = document.querySelector("div.extra-eq input[name='autoeq-from']"); - let autoEQToInput = document.querySelector("div.extra-eq input[name='autoeq-to']"); - autoEQFromInput.value = Equalizer.config.AutoEQRange[0].toFixed(0); - autoEQToInput.value = Equalizer.config.AutoEQRange[1].toFixed(0); - document.querySelector("div.extra-eq button.autoeq").addEventListener("click", () => { - // Generate filters automatically - let phoneSelected = eqPhoneSelect.value; - if (!phoneSelected) { - let firstPhone = eqPhoneSelect.querySelectorAll("option")[1]; - if (firstPhone) { - phoneSelected = eqPhoneSelect.value = firstPhone.value; - } - } - let phoneObj = phoneSelected && activePhones.filter( - p => p.fullName == phoneSelected)[0]; - let targetObj = (activePhones.filter(p => p.isTarget)[0] || - activePhones.filter(p => p !== phoneObj && !p.isTarget)[0]); - if (!phoneObj || !targetObj) { - alert("Please select model and target, if there are no target and multiple models are displayed then the second one will be selected as target."); - return; - } - let autoEQOverlay = document.querySelector(".extra-eq-overlay"); - autoEQOverlay.style.display = "block"; - setTimeout(() => { - let autoEQFrom = Math.min(Math.max(parseInt(autoEQFromInput.value) || 0, 20), 20000); - let autoEQTo = Math.min(Math.max(parseInt(autoEQToInput.value) || 0, autoEQFrom), 20000); - Equalizer.config.AutoEQRange = [autoEQFrom, autoEQTo]; - let phoneCHs = (phoneObj.rawChannels.filter(c => c) - .map(ch => ch.map(([f, v]) => [f, v + phoneObj.norm]))); - let phoneCH = (phoneCHs.length > 1) ? avgCurves(phoneCHs) : phoneCHs[0]; - let targetCH = targetObj.rawChannels.filter(c => c)[0].map(([f, v]) => [f, v + targetObj.norm]); - let filters = Equalizer.autoeq(phoneCH, targetCH, eqBands); - filtersToElem(filters); - applyEQ(); - autoEQOverlay.style.display = "none"; - }, 100); + let exportElem = document.querySelector("#file-filters-export"); + exportElem.href && URL.revokeObjectURL(exportElem.href); + exportElem.href = URL.createObjectURL(new Blob([settings])); + exportElem.download = phoneObj.fullName.replace(/^Uploaded /, "") + " Filters.txt"; + exportElem.click(); + }); + // Export filters as graphic eq (for wavelet) + document + .querySelector("div.extra-eq button.export-graphic-filters") + .addEventListener("click", () => { + let phoneSelected = eqPhoneSelect.value; + let phoneObj = (phoneSelected && + activePhones.filter((p) => p.fullName == phoneSelected && p.eq)[0]) || { + fullName: "Unnamed", + }; + let filters = elemToFilters(); + if (!filters.length) { + alert("Please add atleast one filter before export."); + return; + } + let graphicEQ = Equalizer.as_graphic_eq(filters); + let settings = + "GraphicEQ: " + + graphicEQ.map(([f, gain]) => f.toFixed(0) + " " + gain.toFixed(1)).join("; "); + let exportElem = document.querySelector("#file-filters-export"); + exportElem.href && URL.revokeObjectURL(exportElem.href); + exportElem.href = URL.createObjectURL(new Blob([settings])); + exportElem.download = phoneObj.fullName.replace(/^Uploaded /, "") + " Graphic Filters.txt"; + exportElem.click(); }); - // Tone Generator - let toneGeneratorFromInput = document.querySelector("div.extra-tone-generator input[name='tone-generator-from']"); - let toneGeneratorToInput = document.querySelector("div.extra-tone-generator input[name='tone-generator-to']"); - let toneGeneratorSlider = document.querySelector("div.extra-tone-generator input[name='tone-generator-freq']"); - let toneGeneratorPlayButton = document.querySelector("div.extra-tone-generator .play"); - let toneGeneratorText = document.querySelector("div.extra-tone-generator .freq-text"); - let toneGeneratorContext = null; - let toneGeneratorOsc = null; - let toneGeneratorTimeoutHandle = null - toneGeneratorSlider.addEventListener("input", () => { - let from = Math.min(Math.max(parseInt(toneGeneratorFromInput.value) || 0, 20), 20000); - let to = Math.min(Math.max(parseInt(toneGeneratorToInput.value) || 0, from), 20000); - let position = parseFloat(toneGeneratorSlider.value) || 0; - let freq = Math.round(Math.exp( // Slider move in log scale - Math.log(from) + (Math.log(to) - Math.log(from)) * position)); - toneGeneratorText.innerText = freq; - if (toneGeneratorOsc) { - let t = toneGeneratorContext.currentTime; - toneGeneratorOsc.frequency.cancelScheduledValues(t); - toneGeneratorOsc.frequency.setTargetAtTime(freq, t, 0.2); // Smoother transition but also delay + // Readme + document.querySelector("div.extra-eq button.readme").addEventListener("click", () => { + alert( + "1. If you want to AutoEQ model A to B, display A B and remove target\n" + + "2. Add/Remove bands before AutoEQ may give you a better result\n" + + "3. Curve of PK filter close to 20K is implementation dependent, avoid such filter if you're not sure how your DSP software works\n" + + "4. EQ treble require resonant peak matching and fine tune by ear, keep treble untouched if you're not sure how to do that\n" + + "5. Tone generator is useful to find actual location of peaks and dips, notice the web version may not work on some platform\n", + ); + }); + // AutoEQ + let autoEQFromInput = document.querySelector("div.extra-eq input[name='autoeq-from']"); + let autoEQToInput = document.querySelector("div.extra-eq input[name='autoeq-to']"); + autoEQFromInput.value = Equalizer.config.AutoEQRange[0].toFixed(0); + autoEQToInput.value = Equalizer.config.AutoEQRange[1].toFixed(0); + document.querySelector("div.extra-eq button.autoeq").addEventListener("click", () => { + // Generate filters automatically + let phoneSelected = eqPhoneSelect.value; + if (!phoneSelected) { + let firstPhone = eqPhoneSelect.querySelectorAll("option")[1]; + if (firstPhone) { + phoneSelected = eqPhoneSelect.value = firstPhone.value; + } + } + let phoneObj = phoneSelected && activePhones.filter((p) => p.fullName == phoneSelected)[0]; + let targetObj = + activePhones.filter((p) => p.isTarget)[0] || + activePhones.filter((p) => p !== phoneObj && !p.isTarget)[0]; + if (!phoneObj || !targetObj) { + alert( + "Please select model and target, if there are no target and multiple models are displayed then the second one will be selected as target.", + ); + return; + } + let autoEQOverlay = document.querySelector(".extra-eq-overlay"); + autoEQOverlay.style.display = "block"; + setTimeout(() => { + let autoEQFrom = Math.min(Math.max(parseInt(autoEQFromInput.value) || 0, 20), 20000); + let autoEQTo = Math.min(Math.max(parseInt(autoEQToInput.value) || 0, autoEQFrom), 20000); + Equalizer.config.AutoEQRange = [autoEQFrom, autoEQTo]; + let phoneCHs = phoneObj.rawChannels + .filter((c) => c) + .map((ch) => ch.map(([f, v]) => [f, v + phoneObj.norm])); + let phoneCH = phoneCHs.length > 1 ? avgCurves(phoneCHs) : phoneCHs[0]; + let targetCH = targetObj.rawChannels + .filter((c) => c)[0] + .map(([f, v]) => [f, v + targetObj.norm]); + let filters = Equalizer.autoeq(phoneCH, targetCH, eqBands); + filtersToElem(filters); + applyEQ(); + autoEQOverlay.style.display = "none"; + }, 100); + }); + // Tone Generator + let toneGeneratorFromInput = document.querySelector( + "div.extra-tone-generator input[name='tone-generator-from']", + ); + let toneGeneratorToInput = document.querySelector( + "div.extra-tone-generator input[name='tone-generator-to']", + ); + let toneGeneratorSlider = document.querySelector( + "div.extra-tone-generator input[name='tone-generator-freq']", + ); + let toneGeneratorPlayButton = document.querySelector("div.extra-tone-generator .play"); + let toneGeneratorText = document.querySelector("div.extra-tone-generator .freq-text"); + let toneGeneratorContext = null; + let toneGeneratorOsc = null; + let toneGeneratorTimeoutHandle = null; + toneGeneratorSlider.addEventListener("input", () => { + let from = Math.min(Math.max(parseInt(toneGeneratorFromInput.value) || 0, 20), 20000); + let to = Math.min(Math.max(parseInt(toneGeneratorToInput.value) || 0, from), 20000); + let position = parseFloat(toneGeneratorSlider.value) || 0; + let freq = Math.round( + Math.exp( + // Slider move in log scale + Math.log(from) + (Math.log(to) - Math.log(from)) * position, + ), + ); + toneGeneratorText.innerText = freq; + if (toneGeneratorOsc) { + let t = toneGeneratorContext.currentTime; + toneGeneratorOsc.frequency.cancelScheduledValues(t); + toneGeneratorOsc.frequency.setTargetAtTime(freq, t, 0.2); // Smoother transition but also delay + } + }); + toneGeneratorPlayButton.addEventListener("click", () => { + if (toneGeneratorOsc) { + toneGeneratorOsc.stop(); + toneGeneratorOsc = null; + toneGeneratorPlayButton.innerText = "Play"; + } else { + if (!toneGeneratorContext) { + if (!window.AudioContext) { + alert("Web audio api is disabled, please enable it if you want to use tone generator."); + return; } - }); - toneGeneratorPlayButton.addEventListener("click", () => { - if (toneGeneratorOsc) { - toneGeneratorOsc.stop(); - toneGeneratorOsc = null; - toneGeneratorPlayButton.innerText = "Play"; + toneGeneratorContext = new AudioContext(); + } + toneGeneratorOsc = toneGeneratorContext.createOscillator(); + toneGeneratorOsc.type = "sine"; + toneGeneratorOsc.frequency.value = parseInt(toneGeneratorText.innerText); + toneGeneratorOsc.connect(toneGeneratorContext.destination); + toneGeneratorOsc.start(); + toneGeneratorPlayButton.innerText = "Stop"; + } + }); + + // Wrap up preamp Calculation Function for plugin + let calcEqDevPreamp = (filters) => { + const phoneSelected = eqPhoneSelect.value; + const phoneObj = + phoneSelected && context.activePhones.find((p) => p.fullName === phoneSelected && p.eq); + + return context.Equalizer.calc_preamp( + phoneObj.rawChannels.filter(Boolean)[0], + phoneObj.eq.rawChannels.filter(Boolean)[0], + ); + }; + + /** + * Dynamically load a plugin from a sub-folder passing it the useful context + * @param pluginsToLoad + * @param context + * @returns {Promise} + */ + async function loadPlugins(pluginsToLoad, context) { + for (const pluginPath of pluginsToLoad) { + try { + let initializePlugin; + + if (typeof module !== "undefined" && module.exports) { + // CommonJS environment (e.g., Node.js) + initializePlugin = require(pluginPath); } else { - if (!toneGeneratorContext) { - if (!window.AudioContext) { - alert("Web audio api is disabled, please enable it if you want to use tone generator."); - return; - } - toneGeneratorContext = new AudioContext(); - } - toneGeneratorOsc = toneGeneratorContext.createOscillator(); - toneGeneratorOsc.type = "sine"; - toneGeneratorOsc.frequency.value = parseInt(toneGeneratorText.innerText); - toneGeneratorOsc.connect(toneGeneratorContext.destination); - toneGeneratorOsc.start(); - toneGeneratorPlayButton.innerText = "Stop"; + // ES Module environment (e.g., modern browsers) + const module = await import(pluginPath); + initializePlugin = module.default; } - }); - // Wrap up preamp Calculation Function for plugin - let calcEqDevPreamp = (filters) => { - const phoneSelected = eqPhoneSelect.value; - const phoneObj = phoneSelected && - context.activePhones.find( - (p) => p.fullName === phoneSelected && p.eq - ); - - return context.Equalizer.calc_preamp( - phoneObj.rawChannels.filter(Boolean)[0], - phoneObj.eq.rawChannels.filter(Boolean)[0] - ); - } - - /** - * Dynamically load a plugin from a sub-folder passing it the useful context - * @param pluginsToLoad - * @param context - * @returns {Promise} - */ - async function loadPlugins(pluginsToLoad, context) { - for (const pluginPath of pluginsToLoad) { - try { - let initializePlugin; - - if (typeof module !== 'undefined' && module.exports) { - // CommonJS environment (e.g., Node.js) - initializePlugin = require(pluginPath); - } else { - // ES Module environment (e.g., modern browsers) - const module = await import(pluginPath); - initializePlugin = module.default; - } - - // Call the plugin function with the provided context - await initializePlugin(context); - console.log(`Successfully loaded plugin: ${pluginPath}`); - } catch (error) { - console.error(`Error loading plugin ${pluginPath}:`, error.message); - } - } - } - // Might come from the config.js - let config = {showNetwork:false}; // Hide the extra selection of network based devices for now - - // Load the plugin with the provided functions - if (typeof extraEQplugins !== "undefined") { - loadPlugins(extraEQplugins, { - filtersToElem, // Put Filters back to Html Elements - elemToFilters, // Get Filters from Html Elements - calcEqDevPreamp,// Reuse existing gain calculations - applyEQ, // Apply EQ - config - }); - } + // Call the plugin function with the provided context + await initializePlugin(context); + console.log(`Successfully loaded plugin: ${pluginPath}`); + } catch (error) { + console.error(`Error loading plugin ${pluginPath}:`, error.message); + } + } + } + // Might come from the config.js + let config = { showNetwork: false }; // Hide the extra selection of network based devices for now + + // Load the plugin with the provided functions + if (typeof extraEQplugins !== "undefined") { + loadPlugins(extraEQplugins, { + filtersToElem, // Put Filters back to Html Elements + elemToFilters, // Get Filters from Html Elements + calcEqDevPreamp, // Reuse existing gain calculations + applyEQ, // Apply EQ + config, + }); + } } addExtra(); // Add accessories to the bottom of the page, if configured function addAccessories() { - let accessoriesBar = document.querySelector("div.accessories"), - accessoriesContainer = document.createElement("aside"); - - accessoriesContainer.innerHTML = whichAccessoriesToUse; - accessoriesBar.append(accessoriesContainer); + let accessoriesBar = document.querySelector("div.accessories"), + accessoriesContainer = document.createElement("aside"); + + accessoriesContainer.innerHTML = whichAccessoriesToUse; + accessoriesBar.append(accessoriesContainer); +} +if (accessories) { + addAccessories(); } -if (accessories) { addAccessories(); } // Add header to alt layout function addHeader() { - let graphToolContainer = document.querySelector("div.graphtool"), - altHeaderElem = document.createElement("header"), - headerButton = document.createElement("button"), - headerLogoElem = document.createElement("div"), - headerLogoLink = document.createElement("a"), - headerLogoImg = document.createElement("img"), - headerLogoSpan = document.createElement("span"), - linksList = document.createElement("ul"); - - headerButton.className = "header-button"; - headerLogoElem.className = "logo"; - headerLogoLink.setAttribute('href', site_url); - if (headerLogoText) { - headerLogoSpan.innerText = headerLogoText; - headerLogoLink.append(headerLogoSpan); - } else if (headerLogoImgUrl) { - headerLogoImg.setAttribute("src", headerLogoImgUrl); - headerLogoLink.append(headerLogoImg); + let graphToolContainer = document.querySelector("div.graphtool"), + altHeaderElem = document.createElement("header"), + headerButton = document.createElement("button"), + headerLogoElem = document.createElement("div"), + headerLogoLink = document.createElement("a"), + headerLogoImg = document.createElement("img"), + headerLogoSpan = document.createElement("span"), + linksList = document.createElement("ul"); + + headerButton.className = "header-button"; + headerLogoElem.className = "logo"; + headerLogoLink.setAttribute("href", site_url); + if (headerLogoText) { + headerLogoSpan.innerText = headerLogoText; + headerLogoLink.append(headerLogoSpan); + } else if (headerLogoImgUrl) { + headerLogoImg.setAttribute("src", headerLogoImgUrl); + headerLogoLink.append(headerLogoImg); + } + + altHeaderElem.append(headerButton); + headerLogoElem.append(headerLogoLink); + altHeaderElem.setAttribute("data-links", ""); + altHeaderElem.append(headerLogoElem); + + altHeaderElem.className = "header"; + graphToolContainer.prepend(altHeaderElem); + + linksList.className = "header-links"; + altHeaderElem.append(linksList); + + headerLinks.forEach(function (link) { + let linkContainerElem = document.createElement("li"), + linkElem = document.createElement("a"); + + linkElem.setAttribute("href", link.url); + if (alt_header_new_tab) { + linkElem.setAttribute("target", "_blank"); + } + if (link.external) { + linkElem.setAttribute("target", "_blank"); + linkElem.classList.add("external"); + } + linkElem.textContent = link.name; + linkContainerElem.append(linkElem); + linksList.append(linkContainerElem); + }); + + headerButton.addEventListener("click", function () { + let headerLinksState = altHeaderElem.getAttribute("data-links"); + + if (headerLinksState === "expanded") { + altHeaderElem.setAttribute("data-links", "collapsed"); + } else { + altHeaderElem.setAttribute("data-links", "expanded"); } - - altHeaderElem.append(headerButton); - headerLogoElem.append(headerLogoLink); - altHeaderElem.setAttribute("data-links", ""); - altHeaderElem.append(headerLogoElem); - - altHeaderElem.className = "header"; - graphToolContainer.prepend(altHeaderElem); - - linksList.className = "header-links"; - altHeaderElem.append(linksList); - - headerLinks.forEach(function(link) { - let linkContainerElem = document.createElement("li"), - linkElem = document.createElement("a"); - - linkElem.setAttribute("href", link.url); - if ( alt_header_new_tab ) { linkElem.setAttribute("target", "_blank"); } - if ( link.external ) { linkElem.setAttribute("target", "_blank"); linkElem.classList.add('external'); } - linkElem.textContent = link.name; - linkContainerElem.append(linkElem); - linksList.append(linkContainerElem); - }) - - headerButton.addEventListener("click", function() { - let headerLinksState = altHeaderElem.getAttribute("data-links"); - - if (headerLinksState === "expanded") { - altHeaderElem.setAttribute("data-links", "collapsed"); - } else { - altHeaderElem.setAttribute("data-links", "expanded"); - } - }); + }); +} +if (alt_layout && alt_header) { + addHeader(); } -if (alt_layout && alt_header) { addHeader(); } // Add external links to bar at bottom of page, if configured function addExternalLinks() { - const externalLinksBar = document.querySelector("div.external-links"); - - linkSets.forEach(function(set) { - let setLabelHtml = document.createElement("span"), - setLabelText = set.label, - links = set.links; - - setLabelHtml.textContent = setLabelText; - externalLinksBar.append(setLabelHtml); - - links.forEach(function(link) { - let linkHtml = document.createElement("a"), - linkName = link.name, - linkUrl = link.url; - - linkHtml.textContent = linkName; - linkHtml.setAttribute("href", linkUrl); - externalLinksBar.append(linkHtml); - }); + const externalLinksBar = document.querySelector("div.external-links"); + + linkSets.forEach(function (set) { + let setLabelHtml = document.createElement("span"), + setLabelText = set.label, + links = set.links; + + setLabelHtml.textContent = setLabelText; + externalLinksBar.append(setLabelHtml); + + links.forEach(function (link) { + let linkHtml = document.createElement("a"), + linkName = link.name, + linkUrl = link.url; + + linkHtml.textContent = linkName; + linkHtml.setAttribute("href", linkUrl); + externalLinksBar.append(linkHtml); }); + }); +} +if (externalLinksBar) { + addExternalLinks(); } -if (externalLinksBar) { addExternalLinks(); } // Add tutorial to alt layout function addTutorial() { - let partsPrimary = document.querySelector("section.parts-primary") - graphContainer = document.querySelector("div.graph-sizer"), - manageContainer = document.querySelector("div.manage"), - overlayContainer = document.createElement("div"), - buttonContainer = document.createElement("div"), - descriptionContainer = document.createElement("div"), - zoomButtons = document.querySelectorAll("div.zoom button"); - - overlayContainer.className = "tutorial-overlay"; - graphContainer.prepend(overlayContainer); - - buttonContainer.className = "tutorial-buttons"; - descriptionContainer.className = "tutorial-description"; - - manageContainer.prepend(descriptionContainer); - manageContainer.prepend(buttonContainer); - - tutorialDefinitions.forEach(function(def) { - let defOverlay = document.createElement("div"), - defButton = document.createElement("button"), - defDescription = document.createElement("article"), - defDescriptionCopy = document.createElement("p"); - - defOverlay.setAttribute("tutorial-def", def.name); - defOverlay.setAttribute("tutorial-on", "false"); - defOverlay.className = "overlay-segment"; - defOverlay.setAttribute("style", "flex-basis: "+ def.width +";") - overlayContainer.append(defOverlay); - - defButton.setAttribute("tutorial-def", def.name); - defButton.setAttribute("tutorial-on", "false"); - defButton.className = "button-segment"; - defButton.textContent = def.name; - buttonContainer.append(defButton); - - defDescription.setAttribute("tutorial-def", def.name); - defDescription.setAttribute("tutorial-on", "false"); - defDescription.className = "description-segment"; - defDescriptionCopy.innerHTML = def.description; - defDescription.append(defDescriptionCopy); - descriptionContainer.append(defDescription); - - defButton.addEventListener("click", function() { - let activeStatus = defButton.getAttribute("tutorial-on"), - activeTutorialElements = document.querySelectorAll("[tutorial-on='true']"), - activeOverlay = document.querySelector("div.overlay-segment[tutorial-on='true']"), - activeButton = document.querySelector("button.button-segment[tutorial-on='true']"), - activeDescription = document.querySelector("article.description-segment[tutorial-on='true']"); - - if (activeOverlay) { activeOverlay.setAttribute("tutorial-on", "false"); } - if (activeButton) { activeButton.setAttribute("tutorial-on", "false"); } - - if (activeStatus === "false") { - if (activeDescription) { activeDescription.setAttribute("tutorial-on", "false"); } - - defOverlay.setAttribute("tutorial-on", "true"); - defButton.setAttribute("tutorial-on", "true"); - defDescription.setAttribute("tutorial-on", "true"); - - partsPrimary.setAttribute("tutorial-active", "true"); - disableZoom(); - - // Analytics event - if (analyticsEnabled) { pushEventTag("tutorial_activated", targetWindow, def.name); } - } else { - partsPrimary.setAttribute("tutorial-active", "false"); - } - }); - - defButton.addEventListener("mouseover", function() { - defOverlay.setAttribute("tutorial-hover", "true"); - }); - - defButton.addEventListener("mouseout", function() { - defOverlay.setAttribute("tutorial-hover", "false"); - }); - - defButton.addEventListener("touchend", function() { - defOverlay.setAttribute("tutorial-hover", "false"); - }); + let partsPrimary = document.querySelector("section.parts-primary"); + (graphContainer = document.querySelector("div.graph-sizer")), + (manageContainer = document.querySelector("div.manage")), + (overlayContainer = document.createElement("div")), + (buttonContainer = document.createElement("div")), + (descriptionContainer = document.createElement("div")), + (zoomButtons = document.querySelectorAll("div.zoom button")); + + overlayContainer.className = "tutorial-overlay"; + graphContainer.prepend(overlayContainer); + + buttonContainer.className = "tutorial-buttons"; + descriptionContainer.className = "tutorial-description"; + + manageContainer.prepend(descriptionContainer); + manageContainer.prepend(buttonContainer); + + tutorialDefinitions.forEach(function (def) { + let defOverlay = document.createElement("div"), + defButton = document.createElement("button"), + defDescription = document.createElement("article"), + defDescriptionCopy = document.createElement("p"); + + defOverlay.setAttribute("tutorial-def", def.name); + defOverlay.setAttribute("tutorial-on", "false"); + defOverlay.className = "overlay-segment"; + defOverlay.setAttribute("style", "flex-basis: " + def.width + ";"); + overlayContainer.append(defOverlay); + + defButton.setAttribute("tutorial-def", def.name); + defButton.setAttribute("tutorial-on", "false"); + defButton.className = "button-segment"; + defButton.textContent = def.name; + buttonContainer.append(defButton); + + defDescription.setAttribute("tutorial-def", def.name); + defDescription.setAttribute("tutorial-on", "false"); + defDescription.className = "description-segment"; + defDescriptionCopy.innerHTML = def.description; + defDescription.append(defDescriptionCopy); + descriptionContainer.append(defDescription); + + defButton.addEventListener("click", function () { + let activeStatus = defButton.getAttribute("tutorial-on"), + activeTutorialElements = document.querySelectorAll("[tutorial-on='true']"), + activeOverlay = document.querySelector("div.overlay-segment[tutorial-on='true']"), + activeButton = document.querySelector("button.button-segment[tutorial-on='true']"), + activeDescription = document.querySelector( + "article.description-segment[tutorial-on='true']", + ); + + if (activeOverlay) { + activeOverlay.setAttribute("tutorial-on", "false"); + } + if (activeButton) { + activeButton.setAttribute("tutorial-on", "false"); + } + + if (activeStatus === "false") { + if (activeDescription) { + activeDescription.setAttribute("tutorial-on", "false"); + } + + defOverlay.setAttribute("tutorial-on", "true"); + defButton.setAttribute("tutorial-on", "true"); + defDescription.setAttribute("tutorial-on", "true"); + + partsPrimary.setAttribute("tutorial-active", "true"); + disableZoom(); + + // Analytics event + if (analyticsEnabled) { + pushEventTag("tutorial_activated", targetWindow, def.name); + } + } else { + partsPrimary.setAttribute("tutorial-active", "false"); + } }); - - // Disable zoom if tutorial is engaged - function disableZoom() { - let activeZoomButton = document.querySelector("div.zoom button.selected"); - - if (activeZoomButton) { activeZoomButton.click(); } - } - - // Disable tutorial if zoom is engaged - zoomButtons.forEach(function(button) { - button.addEventListener("click", function() { - let tutorialState = document.querySelector("section.parts-primary").getAttribute("tutorial-active"); - - if (button.classList.contains("selected") && tutorialState === "true") { - let activeOverlay = document.querySelector("div.overlay-segment[tutorial-on='true']"), - activeButton = document.querySelector("button.button-segment[tutorial-on='true']"), - activeDescription = document.querySelector("article.description-segment[tutorial-on='true']"); - - document.querySelector("section.parts-primary").setAttribute("tutorial-active","false"); - activeOverlay.setAttribute("tutorial-on", "false"); - activeButton.setAttribute("tutorial-on", "false"); - } - }); + + defButton.addEventListener("mouseover", function () { + defOverlay.setAttribute("tutorial-hover", "true"); + }); + + defButton.addEventListener("mouseout", function () { + defOverlay.setAttribute("tutorial-hover", "false"); + }); + + defButton.addEventListener("touchend", function () { + defOverlay.setAttribute("tutorial-hover", "false"); + }); + }); + + // Disable zoom if tutorial is engaged + function disableZoom() { + let activeZoomButton = document.querySelector("div.zoom button.selected"); + + if (activeZoomButton) { + activeZoomButton.click(); + } + } + + // Disable tutorial if zoom is engaged + zoomButtons.forEach(function (button) { + button.addEventListener("click", function () { + let tutorialState = document + .querySelector("section.parts-primary") + .getAttribute("tutorial-active"); + + if (button.classList.contains("selected") && tutorialState === "true") { + let activeOverlay = document.querySelector("div.overlay-segment[tutorial-on='true']"), + activeButton = document.querySelector("button.button-segment[tutorial-on='true']"), + activeDescription = document.querySelector( + "article.description-segment[tutorial-on='true']", + ); + + document.querySelector("section.parts-primary").setAttribute("tutorial-active", "false"); + activeOverlay.setAttribute("tutorial-on", "false"); + activeButton.setAttribute("tutorial-on", "false"); + } }); + }); +} +if (alt_tutorial) { + addTutorial(); } -if (alt_tutorial) { addTutorial(); } // Set active graph site link function setActiveDatabase() { - let url = targetWindow.location.href, - dbLinks = document.querySelectorAll("div.external-links a"); + let url = targetWindow.location.href, + dbLinks = document.querySelectorAll("div.external-links a"); - dbLinks.forEach(function(link) { - let linkUrl = link.getAttribute("href"); + dbLinks.forEach(function (link) { + let linkUrl = link.getAttribute("href"); - if ( url.includes(linkUrl) ) { - link.setAttribute("class", "active"); - } - }); + if (url.includes(linkUrl)) { + link.setAttribute("class", "active"); + } + }); } setActiveDatabase(); // Expand / collapse function function toggleExpandCollapse() { - const graphIsIframe = (window.top !== window.self) ? true:false, - graphBody = document.querySelector("body"), - parentBody = window.top.document.querySelector("body"), - expandCollapseButton = document.querySelector("button#expand-collapse"); - - - if ( graphIsIframe) { graphBody.setAttribute("data-graph-frame", "collapsed"); } - - - if ( graphIsIframe && expandableOnly ) { - const expandOnlyMax = ( expandableOnly === true ) ? 1000000:expandableOnly, - expandOnlyStyle = document.createElement("style"), - expandOnlyCss = ` - @media ( max-width: `+ expandOnlyMax +`px ) { - body[data-expandable="only"][data-graph-frame="collapsed"] { - overflow: hidden; - } - - body[data-expandable="only"][data-graph-frame="collapsed"] div.expand-collapse { - position: fixed; - top: 0; - left: 0; - - display: flex; - justify-content: center; - align-items: center; - - width: 100%; - height: 100%; - padding: 0; - - background-color: var(--background-color); - background-color: transparent; - border: none; - } - - body[data-expandable="only"][data-graph-frame="collapsed"] div.expand-collapse:after { - position: absolute; - - content: 'Tap to launch graph tool'; - - color: var(--font-color-primary); - font-family: var(--font-secondary); - font-size: 11px; - line-height: 1em; - text-transform: uppercase; - - pointer-events: none; - } - - body[data-expandable="only"][data-graph-frame="collapsed"] div.expand-collapse button#expand-collapse { - display: flex; - justify-content: center; - align-items: center; - - width: 100%; - height: 100%; - - background-color: transparent; - } - - body[data-expandable="only"][data-graph-frame="collapsed"] div.expand-collapse button#expand-collapse:before { - position: relative; - z-index: 1; - - transform: scale(7); - } - - body[data-expandable="only"][data-graph-frame="collapsed"] div.expand-collapse button#expand-collapse:after { - position: absolute; - top: 0; - left: 0; - - content: ''; - - display: block; - width: 100%; - height: 100%; - - background-color: var(--background-color); - - opacity: 0.9; - } - - body[data-expandable="only"][data-graph-frame="collapsed"] section.parts-primary { - flex: 100% 1 1; - overflow: hidden; - } - - body[data-expandable="only"][data-graph-frame="collapsed"] section.parts-secondary { - display: none; - } - } + const graphIsIframe = window.top !== window.self ? true : false, + graphBody = document.querySelector("body"), + parentBody = window.top.document.querySelector("body"), + expandCollapseButton = document.querySelector("button#expand-collapse"); + + if (graphIsIframe) { + graphBody.setAttribute("data-graph-frame", "collapsed"); + } + + if (graphIsIframe && expandableOnly) { + const expandOnlyMax = expandableOnly === true ? 1000000 : expandableOnly, + expandOnlyStyle = document.createElement("style"), + expandOnlyCss = + ` + @media ( max-width: ` + + expandOnlyMax + + `px ) { + body[data-expandable="only"][data-graph-frame="collapsed"] { + overflow: hidden; + } + + body[data-expandable="only"][data-graph-frame="collapsed"] div.expand-collapse { + position: fixed; + top: 0; + left: 0; + + display: flex; + justify-content: center; + align-items: center; + + width: 100%; + height: 100%; + padding: 0; + + background-color: var(--background-color); + background-color: transparent; + border: none; + } + + body[data-expandable="only"][data-graph-frame="collapsed"] div.expand-collapse:after { + position: absolute; + + content: 'Tap to launch graph tool'; + + color: var(--font-color-primary); + font-family: var(--font-secondary); + font-size: 11px; + line-height: 1em; + text-transform: uppercase; + + pointer-events: none; + } + + body[data-expandable="only"][data-graph-frame="collapsed"] div.expand-collapse button#expand-collapse { + display: flex; + justify-content: center; + align-items: center; + + width: 100%; + height: 100%; + + background-color: transparent; + } + + body[data-expandable="only"][data-graph-frame="collapsed"] div.expand-collapse button#expand-collapse:before { + position: relative; + z-index: 1; + + transform: scale(7); + } + + body[data-expandable="only"][data-graph-frame="collapsed"] div.expand-collapse button#expand-collapse:after { + position: absolute; + top: 0; + left: 0; + + content: ''; + + display: block; + width: 100%; + height: 100%; + + background-color: var(--background-color); + + opacity: 0.9; + } + + body[data-expandable="only"][data-graph-frame="collapsed"] section.parts-primary { + flex: 100% 1 1; + overflow: hidden; + } + + body[data-expandable="only"][data-graph-frame="collapsed"] section.parts-secondary { + display: none; + } + } `; - - expandOnlyStyle.textContent = expandOnlyCss; - expandOnlyStyle.setAttribute("type", "text/css"); - document.querySelector("body").append(expandOnlyStyle); - - graphBody.setAttribute("data-expandable", "only"); - } else if ( graphIsIframe && expandable ) { - graphBody.setAttribute("data-expandable", "true"); - } - - const parentStyle = window.top.document.createElement("style"), - parentCss = ` - :root { - --header-height: `+ headerHeight +`; - } - - body[data-graph-frame="expanded"] { - width: 100%; - height: 100%; - max-height: -webkit-fill-available; - overflow: hidden; - } - - body[data-graph-frame="expanded"] button.graph-frame-collapse { - display: inherit; - } - - body[data-graph-frame="expanded"] iframe#GraphTool { - position: fixed; - top: var(--header-height); - left: 0; - - width: 100% !important; - height: calc(100% - var(--header-height)) !important; - - animation-name: graph-tool-expand; - animation-duration: 0.15s; - animation-iteration-count: 1; - animation-timing-function: ease-out; - animation-fill-mode: forwards; - } - @keyframes graph-tool-expand { - 0% { - position: relative; - opacity: 1.0; - transform: scale(1.0); - } - 48% { - position: relative; - opacity: 0.0; - transform: scale(0.9); - } - 50% { - position: fixed; - opacity: 0.0; - transform: scale(0.9); - } - 52% { - position: fixed; - opacity: 0.0; - transform: scale(0.9); - } - 100% { - position: fixed; - opacity: 1.0; - transform: scale(1.0); - } - }`; - - parentStyle.textContent = parentCss; - parentStyle.setAttribute("type", "text/css"); - parentBody.append(parentStyle); - - expandCollapseButton.addEventListener("click", function(e) { - let frameState = document.querySelector("body").getAttribute("data-graph-frame"); - - if ( frameState === "expanded" ) { - graphBody.setAttribute("data-graph-frame", "collapsed"); - parentBody.setAttribute("data-graph-frame", "collapsed"); - } else { - graphBody.setAttribute("data-graph-frame", "expanded"); - parentBody.setAttribute("data-graph-frame", "expanded"); + expandOnlyStyle.textContent = expandOnlyCss; + expandOnlyStyle.setAttribute("type", "text/css"); + document.querySelector("body").append(expandOnlyStyle); + + graphBody.setAttribute("data-expandable", "only"); + } else if (graphIsIframe && expandable) { + graphBody.setAttribute("data-expandable", "true"); + } + + const parentStyle = window.top.document.createElement("style"), + parentCss = + ` + :root { + --header-height: ` + + headerHeight + + `; + } + + body[data-graph-frame="expanded"] { + width: 100%; + height: 100%; + max-height: -webkit-fill-available; + overflow: hidden; + } + + body[data-graph-frame="expanded"] button.graph-frame-collapse { + display: inherit; + } + + body[data-graph-frame="expanded"] iframe#GraphTool { + position: fixed; + top: var(--header-height); + left: 0; + + width: 100% !important; + height: calc(100% - var(--header-height)) !important; + + animation-name: graph-tool-expand; + animation-duration: 0.15s; + animation-iteration-count: 1; + animation-timing-function: ease-out; + animation-fill-mode: forwards; + } + + @keyframes graph-tool-expand { + 0% { + position: relative; + opacity: 1.0; + transform: scale(1.0); } - - e.stopPropagation(); - }); - + 48% { + position: relative; + opacity: 0.0; + transform: scale(0.9); + } + 50% { + position: fixed; + opacity: 0.0; + transform: scale(0.9); + } + 52% { + position: fixed; + opacity: 0.0; + transform: scale(0.9); + } + 100% { + position: fixed; + opacity: 1.0; + transform: scale(1.0); + } + }`; + + parentStyle.textContent = parentCss; + parentStyle.setAttribute("type", "text/css"); + parentBody.append(parentStyle); + + expandCollapseButton.addEventListener("click", function (e) { + let frameState = document.querySelector("body").getAttribute("data-graph-frame"); + + if (frameState === "expanded") { + graphBody.setAttribute("data-graph-frame", "collapsed"); + parentBody.setAttribute("data-graph-frame", "collapsed"); + } else { + graphBody.setAttribute("data-graph-frame", "expanded"); + parentBody.setAttribute("data-graph-frame", "expanded"); + } + + e.stopPropagation(); + }); } -if ( expandable && accessDocumentTop ) { toggleExpandCollapse(); } +if (expandable && accessDocumentTop) { + toggleExpandCollapse(); +} // Update user config for target + baseline function setUserConfig() { - let urlObj = new URL(document.URL), - pathClean = urlObj.pathname.replace(/\W/g, ""), - configName = pathClean.length > 0 ? "_" + pathClean + "_a" : "_a", - configJson = { - "phones": [], - "normalMode": (norm_sel === 1) ? "Hz" : "dB", - "normalValue": (norm_sel === 1) ? norm_fr : norm_phon - }, - activeBaseline = baseline.p ? baseline.p.fileName : 0; - - activePhones.forEach(function(phone) { - let phoneJson = {}, - fullName = phone.fullName, - fileName = phone.fileName, - isTarget = phone.isTarget ? phone.isTarget : false, - isHidden = phone.hide ? phone.hide : false, - isBaseline = fileName === activeBaseline ? true : false, - isPinned = phone.pin ? phone.pin : false; - - if (isTarget || isBaseline) { - phoneJson.fullName = fullName; - phoneJson.fileName = fileName; - phoneJson.isTarget = isTarget; - phoneJson.isHidden = isHidden; - phoneJson.isBaseline = isBaseline; - phoneJson.isPinned = isPinned; - - configJson.phones.push(phoneJson); - } - }); - - localStorage.setItem("userConfig" + configName, JSON.stringify(configJson)); + let urlObj = new URL(document.URL), + pathClean = urlObj.pathname.replace(/\W/g, ""), + configName = pathClean.length > 0 ? "_" + pathClean + "_a" : "_a", + configJson = { + phones: [], + normalMode: norm_sel === 1 ? "Hz" : "dB", + normalValue: norm_sel === 1 ? norm_fr : norm_phon, + }, + activeBaseline = baseline.p ? baseline.p.fileName : 0; + + activePhones.forEach(function (phone) { + let phoneJson = {}, + fullName = phone.fullName, + fileName = phone.fileName, + isTarget = phone.isTarget ? phone.isTarget : false, + isHidden = phone.hide ? phone.hide : false, + isBaseline = fileName === activeBaseline ? true : false, + isPinned = phone.pin ? phone.pin : false; + + if (isTarget || isBaseline) { + phoneJson.fullName = fullName; + phoneJson.fileName = fileName; + phoneJson.isTarget = isTarget; + phoneJson.isHidden = isHidden; + phoneJson.isBaseline = isBaseline; + phoneJson.isPinned = isPinned; + + configJson.phones.push(phoneJson); + } + }); + + localStorage.setItem("userConfig" + configName, JSON.stringify(configJson)); } // Insert user config phones to inits function userConfigAppendInits(initReq) { - if (targetRestoreLastUsed) { - let urlObj = new URL(document.URL), - pathClean = urlObj.pathname.replace(/\W/g, ""), - configName = pathClean.length > 0 ? "_" + pathClean + "_a" : "_a", - configJson = JSON.parse(localStorage.getItem("userConfig" + configName)), - configNumOfPhones = configJson ? configJson.phones.length : 0; - - if (configJson && configNumOfPhones) { - initReq.slice(0).forEach(function(item) { - if (item.endsWith(' Target')) { - initReq.splice(initReq.indexOf(item), 1); - } - }); - - configJson.phones.forEach(function(phone) { - if (!initReq.includes(phone.fileName)) { - initReq.push(phone.fileName); - } - }); + if (targetRestoreLastUsed) { + let urlObj = new URL(document.URL), + pathClean = urlObj.pathname.replace(/\W/g, ""), + configName = pathClean.length > 0 ? "_" + pathClean + "_a" : "_a", + configJson = JSON.parse(localStorage.getItem("userConfig" + configName)), + configNumOfPhones = configJson ? configJson.phones.length : 0; + + if (configJson && configNumOfPhones) { + initReq.slice(0).forEach(function (item) { + if (item.endsWith(" Target")) { + initReq.splice(initReq.indexOf(item), 1); } + }); + + configJson.phones.forEach(function (phone) { + if (!initReq.includes(phone.fileName)) { + initReq.push(phone.fileName); + } + }); } + } } // Apply baseline and hide settings function userConfigApplyViewSettings(phoneInTable) { - if (targetRestoreLastUsed) { - userConfigApplicationActive = 1; - - let urlObj = new URL(document.URL), - pathClean = urlObj.pathname.replace(/\W/g, ""), - configName = pathClean.length > 0 ? "_" + pathClean + "_a" : "_a", - configJson = JSON.parse(localStorage.getItem("userConfig" + configName)); - - if (configJson) { - let phone = configJson.phones.find(item => item.fileName === phoneInTable); - - if (typeof phone !== "undefined") { - let row = document.querySelector("tr[data-filename='"+ phone.fileName +"']"), - hideButton = row.querySelector("td.hideIcon"), - baselineButton = row.querySelector("td.button-baseline"), - pinButton = row.querySelector("td.button-pin"); - - if (phone.isHidden && !hideButton.classList.contains("selected")) { - hideButton.click(); - } - - if (phone.isBaseline && !baselineButton.classList.contains("selected")) { - baselineButton.click(); - } - - if (phone.isPinned && pinButton.getAttribute('data-pinned') !== "true") { - pinButton.click(); - } - } + if (targetRestoreLastUsed) { + userConfigApplicationActive = 1; + + let urlObj = new URL(document.URL), + pathClean = urlObj.pathname.replace(/\W/g, ""), + configName = pathClean.length > 0 ? "_" + pathClean + "_a" : "_a", + configJson = JSON.parse(localStorage.getItem("userConfig" + configName)); + + if (configJson) { + let phone = configJson.phones.find((item) => item.fileName === phoneInTable); + + if (typeof phone !== "undefined") { + let row = document.querySelector("tr[data-filename='" + phone.fileName + "']"), + hideButton = row.querySelector("td.hideIcon"), + baselineButton = row.querySelector("td.button-baseline"), + pinButton = row.querySelector("td.button-pin"); + + if (phone.isHidden && !hideButton.classList.contains("selected")) { + hideButton.click(); } - userConfigApplicationActive = 0; + if (phone.isBaseline && !baselineButton.classList.contains("selected")) { + baselineButton.click(); + } + + if (phone.isPinned && pinButton.getAttribute("data-pinned") !== "true") { + pinButton.click(); + } + } } -}; + + userConfigApplicationActive = 0; + } +} // Apply normalization config function userConfigApplyNormalization() { - userConfigApplicationActive = 1; - - let urlObj = new URL(document.URL), - pathClean = urlObj.pathname.replace(/\W/g, ""), - configName = pathClean.length > 0 ? "_" + pathClean + "_a" : "_a", - configJson = JSON.parse(localStorage.getItem("userConfig" + configName)); - - if ( configJson && configJson.normalMode === "Hz" ) { - document.querySelector("input#norm-fr").value = configJson.normalValue; - document.querySelector("input#norm-fr").dispatchEvent(new Event("change")); - } else if ( configJson && configJson.normalMode === "dB" ) { - document.querySelector("input#norm-phon").value = configJson.normalValue; - document.querySelector("input#norm-phon").dispatchEvent(new Event("change")); - } - - userConfigApplicationActive = 0; + userConfigApplicationActive = 1; + + let urlObj = new URL(document.URL), + pathClean = urlObj.pathname.replace(/\W/g, ""), + configName = pathClean.length > 0 ? "_" + pathClean + "_a" : "_a", + configJson = JSON.parse(localStorage.getItem("userConfig" + configName)); + + if (configJson && configJson.normalMode === "Hz") { + document.querySelector("input#norm-fr").value = configJson.normalValue; + document.querySelector("input#norm-fr").dispatchEvent(new Event("change")); + } else if (configJson && configJson.normalMode === "dB") { + document.querySelector("input#norm-phon").value = configJson.normalValue; + document.querySelector("input#norm-phon").dispatchEvent(new Event("change")); + } + + userConfigApplicationActive = 0; } diff --git a/iframe.html b/iframe.html index 2428e8b..0ad8c2c 100644 --- a/iframe.html +++ b/iframe.html @@ -1,146 +1,298 @@ - - + + - - - - - - -
- Header -
- -
-

Viverra tellus in hac

- -

Lorem ipsum dolor sit amet, consxectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Quisque non tellus orci ac. Dictumst quisque sagittis purus sit amet volutpat consequat. Vitae nunc sed velit dignissim sodales ut. Faucibus ornare suspendisse sed nisi lacus sed viverra tellus in. Dignissim enim sit amet venenatis urna cursus eget nunc. Mi proin sed libero enim. Ut sem viverra aliquet eget sit amet. Integer enim neque volutpat ac tincidunt vitae. Tincidunt nunc pulvinar sapien et ligula ullamcorper malesuada. Mauris rhoncus aenean vel elit scelerisque mauris pellentesque. Lacus luctus accumsan tortor posuere ac ut consequat semper. Non pulvinar neque laoreet suspendisse interdum consectetur libero id faucibus. Aliquam sem et tortor consequat id. Cursus sit amet dictum sit amet justo donec. Donec adipiscing tristique risus nec feugiat in fermentum posuere.

- -

Diam donec adipiscing tristique risus nec. Amet nisl purus in mollis. Et malesuada fames ac turpis egestas maecenas pharetra. Ante metus dictum at tempor commodo ullamcorper a. Dui id ornare arcu odio ut sem nulla. Ut pharetra sit amet aliquam id diam maecenas. Scelerisque in dictum non consectetur a erat nam at. In ante metus dictum at tempor. Eget nulla facilisi etiam dignissim diam quis enim lobortis scelerisque. Euismod nisi porta lorem mollis aliquam ut porttitor leo a. Malesuada proin libero nunc consequat interdum. Turpis egestas sed tempus urna et pharetra pharetra massa massa. Quis blandit turpis cursus in hac habitasse. Amet commodo nulla facilisi nullam vehicula ipsum a.

- -

Mauris ultrices eros in cursus turpis massa tincidunt. Aliquam ut porttitor leo a diam sollicitudin. Curabitur vitae nunc sed velit. Cursus metus aliquam eleifend mi in nulla posuere sollicitudin. Lectus nulla at volutpat diam ut. Nibh nisl condimentum id venenatis a condimentum vitae sapien. Tincidunt id aliquet risus feugiat in ante metus. Elementum nibh tellus molestie nunc non blandit massa enim. Ac tortor vitae purus faucibus ornare suspendisse. Pellentesque sit amet porttitor eget. Commodo quis imperdiet massa tincidunt. Nunc sed id semper risus in hendrerit gravida. Proin nibh nisl condimentum id venenatis a condimentum. Tortor at risus viverra adipiscing at in. Pharetra massa massa ultricies mi quis hendrerit dolor. Tempor id eu nisl nunc mi ipsum faucibus vitae.

- -

Tellus orci

- -

Viverra mauris in aliquam sem. Viverra tellus in hac habitasse platea. Facilisi nullam vehicula ipsum a arcu cursus. Nunc sed augue lacus viverra vitae congue eu. Pretium fusce id velit ut tortor pretium viverra suspendisse. Eu scelerisque felis imperdiet proin. Tincidunt arcu non sodales neque sodales ut etiam sit amet. Tellus at urna condimentum mattis pellentesque. Congue nisi vitae suscipit tellus. Ut morbi tincidunt augue interdum.

- -

Scelerisque in dictum non consectetur a. Elit pellentesque habitant morbi tristique senectus et. Nulla aliquet enim tortor at auctor urna nunc id. In ornare quam viverra orci. Auctor eu augue ut lectus arcu bibendum at varius vel. In cursus turpis massa tincidunt dui ut ornare lectus. Accumsan in nisl nisi scelerisque eu ultrices vitae auctor eu. A diam sollicitudin tempor id. Tellus mauris a diam maecenas sed enim ut sem. Pellentesque id nibh tortor id aliquet lectus proin. Fermentum et sollicitudin ac orci phasellus. Dolor morbi non arcu risus quis. Bibendum enim facilisis gravida neque. Tellus in metus vulputate eu scelerisque felis. Integer malesuada nunc vel risus commodo. Lacus laoreet non curabitur gravida arcu.

-
- - - -
- -
- -
-

Viverra tellus in hac

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Quisque non tellus orci ac. Dictumst quisque sagittis purus sit amet volutpat consequat. Vitae nunc sed velit dignissim sodales ut. Faucibus ornare suspendisse sed nisi lacus sed viverra tellus in. Dignissim enim sit amet venenatis urna cursus eget nunc. Mi proin sed libero enim. Ut sem viverra aliquet eget sit amet. Integer enim neque volutpat ac tincidunt vitae. Tincidunt nunc pulvinar sapien et ligula ullamcorper malesuada. Mauris rhoncus aenean vel elit scelerisque mauris pellentesque. Lacus luctus accumsan tortor posuere ac ut consequat semper. Non pulvinar neque laoreet suspendisse interdum consectetur libero id faucibus. Aliquam sem et tortor consequat id. Cursus sit amet dictum sit amet justo donec. Donec adipiscing tristique risus nec feugiat in fermentum posuere.

+
+ +
-

Diam donec adipiscing tristique risus nec. Amet nisl purus in mollis. Et malesuada fames ac turpis egestas maecenas pharetra. Ante metus dictum at tempor commodo ullamcorper a. Dui id ornare arcu odio ut sem nulla. Ut pharetra sit amet aliquam id diam maecenas. Scelerisque in dictum non consectetur a erat nam at. In ante metus dictum at tempor. Eget nulla facilisi etiam dignissim diam quis enim lobortis scelerisque. Euismod nisi porta lorem mollis aliquam ut porttitor leo a. Malesuada proin libero nunc consequat interdum. Turpis egestas sed tempus urna et pharetra pharetra massa massa. Quis blandit turpis cursus in hac habitasse. Amet commodo nulla facilisi nullam vehicula ipsum a.

+
+

Viverra tellus in hac

-

Mauris ultrices eros in cursus turpis massa tincidunt. Aliquam ut porttitor leo a diam sollicitudin. Curabitur vitae nunc sed velit. Cursus metus aliquam eleifend mi in nulla posuere sollicitudin. Lectus nulla at volutpat diam ut. Nibh nisl condimentum id venenatis a condimentum vitae sapien. Tincidunt id aliquet risus feugiat in ante metus. Elementum nibh tellus molestie nunc non blandit massa enim. Ac tortor vitae purus faucibus ornare suspendisse. Pellentesque sit amet porttitor eget. Commodo quis imperdiet massa tincidunt. Nunc sed id semper risus in hendrerit gravida. Proin nibh nisl condimentum id venenatis a condimentum. Tortor at risus viverra adipiscing at in. Pharetra massa massa ultricies mi quis hendrerit dolor. Tempor id eu nisl nunc mi ipsum faucibus vitae.

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Quisque non tellus orci ac. Dictumst + quisque sagittis purus sit amet volutpat consequat. Vitae nunc sed velit dignissim sodales + ut. Faucibus ornare suspendisse sed nisi lacus sed viverra tellus in. Dignissim enim sit + amet venenatis urna cursus eget nunc. Mi proin sed libero enim. Ut sem viverra aliquet eget + sit amet. Integer enim neque volutpat ac tincidunt vitae. Tincidunt nunc pulvinar sapien et + ligula ullamcorper malesuada. Mauris rhoncus aenean vel elit scelerisque mauris + pellentesque. Lacus luctus accumsan tortor posuere ac ut consequat semper. Non pulvinar + neque laoreet suspendisse interdum consectetur libero id faucibus. Aliquam sem et tortor + consequat id. Cursus sit amet dictum sit amet justo donec. Donec adipiscing tristique risus + nec feugiat in fermentum posuere. +

-

Tellus orci

+

+ Diam donec adipiscing tristique risus nec. Amet nisl purus in mollis. Et malesuada fames ac + turpis egestas maecenas pharetra. Ante metus dictum at tempor commodo ullamcorper a. Dui id + ornare arcu odio ut sem nulla. Ut pharetra sit amet aliquam id diam maecenas. Scelerisque in + dictum non consectetur a erat nam at. In ante metus dictum at tempor. Eget nulla facilisi + etiam dignissim diam quis enim lobortis scelerisque. Euismod nisi porta lorem mollis aliquam + ut porttitor leo a. Malesuada proin libero nunc consequat interdum. Turpis egestas sed + tempus urna et pharetra pharetra massa massa. Quis blandit turpis cursus in hac habitasse. + Amet commodo nulla facilisi nullam vehicula ipsum a. +

-

Viverra mauris in aliquam sem. Viverra tellus in hac habitasse platea. Facilisi nullam vehicula ipsum a arcu cursus. Nunc sed augue lacus viverra vitae congue eu. Pretium fusce id velit ut tortor pretium viverra suspendisse. Eu scelerisque felis imperdiet proin. Tincidunt arcu non sodales neque sodales ut etiam sit amet. Tellus at urna condimentum mattis pellentesque. Congue nisi vitae suscipit tellus. Ut morbi tincidunt augue interdum.

+

+ Mauris ultrices eros in cursus turpis massa tincidunt. Aliquam ut porttitor leo a diam + sollicitudin. Curabitur vitae nunc sed velit. Cursus metus aliquam eleifend mi in nulla + posuere sollicitudin. Lectus nulla at volutpat diam ut. Nibh nisl condimentum id venenatis a + condimentum vitae sapien. Tincidunt id aliquet risus feugiat in ante metus. Elementum nibh + tellus molestie nunc non blandit massa enim. Ac tortor vitae purus faucibus ornare + suspendisse. Pellentesque sit amet porttitor eget. Commodo quis imperdiet massa tincidunt. + Nunc sed id semper risus in hendrerit gravida. Proin nibh nisl condimentum id venenatis a + condimentum. Tortor at risus viverra adipiscing at in. Pharetra massa massa ultricies mi + quis hendrerit dolor. Tempor id eu nisl nunc mi ipsum faucibus vitae. +

-

Scelerisque in dictum non consectetur a. Elit pellentesque habitant morbi tristique senectus et. Nulla aliquet enim tortor at auctor urna nunc id. In ornare quam viverra orci. Auctor eu augue ut lectus arcu bibendum at varius vel. In cursus turpis massa tincidunt dui ut ornare lectus. Accumsan in nisl nisi scelerisque eu ultrices vitae auctor eu. A diam sollicitudin tempor id. Tellus mauris a diam maecenas sed enim ut sem. Pellentesque id nibh tortor id aliquet lectus proin. Fermentum et sollicitudin ac orci phasellus. Dolor morbi non arcu risus quis. Bibendum enim facilisis gravida neque. Tellus in metus vulputate eu scelerisque felis. Integer malesuada nunc vel risus commodo. Lacus laoreet non curabitur gravida arcu.

-
- -
- -
- -
-

Viverra tellus in hac

+

Tellus orci

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Quisque non tellus orci ac. Dictumst quisque sagittis purus sit amet volutpat consequat. Vitae nunc sed velit dignissim sodales ut. Faucibus ornare suspendisse sed nisi lacus sed viverra tellus in. Dignissim enim sit amet venenatis urna cursus eget nunc. Mi proin sed libero enim. Ut sem viverra aliquet eget sit amet. Integer enim neque volutpat ac tincidunt vitae. Tincidunt nunc pulvinar sapien et ligula ullamcorper malesuada. Mauris rhoncus aenean vel elit scelerisque mauris pellentesque. Lacus luctus accumsan tortor posuere ac ut consequat semper. Non pulvinar neque laoreet suspendisse interdum consectetur libero id faucibus. Aliquam sem et tortor consequat id. Cursus sit amet dictum sit amet justo donec. Donec adipiscing tristique risus nec feugiat in fermentum posuere.

+

+ Viverra mauris in aliquam sem. Viverra tellus in hac habitasse platea. Facilisi nullam + vehicula ipsum a arcu cursus. Nunc sed augue lacus viverra vitae congue eu. Pretium fusce id + velit ut tortor pretium viverra suspendisse. Eu scelerisque felis imperdiet proin. Tincidunt + arcu non sodales neque sodales ut etiam sit amet. Tellus at urna condimentum mattis + pellentesque. Congue nisi vitae suscipit tellus. Ut morbi tincidunt augue interdum. +

-

Diam donec adipiscing tristique risus nec. Amet nisl purus in mollis. Et malesuada fames ac turpis egestas maecenas pharetra. Ante metus dictum at tempor commodo ullamcorper a. Dui id ornare arcu odio ut sem nulla. Ut pharetra sit amet aliquam id diam maecenas. Scelerisque in dictum non consectetur a erat nam at. In ante metus dictum at tempor. Eget nulla facilisi etiam dignissim diam quis enim lobortis scelerisque. Euismod nisi porta lorem mollis aliquam ut porttitor leo a. Malesuada proin libero nunc consequat interdum. Turpis egestas sed tempus urna et pharetra pharetra massa massa. Quis blandit turpis cursus in hac habitasse. Amet commodo nulla facilisi nullam vehicula ipsum a.

+

+ Scelerisque in dictum non consectetur a. Elit pellentesque habitant morbi tristique senectus + et. Nulla aliquet enim tortor at auctor urna nunc id. In ornare quam viverra orci. Auctor eu + augue ut lectus arcu bibendum at varius vel. In cursus turpis massa tincidunt dui ut ornare + lectus. Accumsan in nisl nisi scelerisque eu ultrices vitae auctor eu. A diam sollicitudin + tempor id. Tellus mauris a diam maecenas sed enim ut sem. Pellentesque id nibh tortor id + aliquet lectus proin. Fermentum et sollicitudin ac orci phasellus. Dolor morbi non arcu + risus quis. Bibendum enim facilisis gravida neque. Tellus in metus vulputate eu scelerisque + felis. Integer malesuada nunc vel risus commodo. Lacus laoreet non curabitur gravida arcu. +

+
-

Mauris ultrices eros in cursus turpis massa tincidunt. Aliquam ut porttitor leo a diam sollicitudin. Curabitur vitae nunc sed velit. Cursus metus aliquam eleifend mi in nulla posuere sollicitudin. Lectus nulla at volutpat diam ut. Nibh nisl condimentum id venenatis a condimentum vitae sapien. Tincidunt id aliquet risus feugiat in ante metus. Elementum nibh tellus molestie nunc non blandit massa enim. Ac tortor vitae purus faucibus ornare suspendisse. Pellentesque sit amet porttitor eget. Commodo quis imperdiet massa tincidunt. Nunc sed id semper risus in hendrerit gravida. Proin nibh nisl condimentum id venenatis a condimentum. Tortor at risus viverra adipiscing at in. Pharetra massa massa ultricies mi quis hendrerit dolor. Tempor id eu nisl nunc mi ipsum faucibus vitae.

+
+ +
-

Tellus orci

+
+

Viverra tellus in hac

-

Viverra mauris in aliquam sem. Viverra tellus in hac habitasse platea. Facilisi nullam vehicula ipsum a arcu cursus. Nunc sed augue lacus viverra vitae congue eu. Pretium fusce id velit ut tortor pretium viverra suspendisse. Eu scelerisque felis imperdiet proin. Tincidunt arcu non sodales neque sodales ut etiam sit amet. Tellus at urna condimentum mattis pellentesque. Congue nisi vitae suscipit tellus. Ut morbi tincidunt augue interdum.

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Quisque non tellus orci ac. Dictumst + quisque sagittis purus sit amet volutpat consequat. Vitae nunc sed velit dignissim sodales + ut. Faucibus ornare suspendisse sed nisi lacus sed viverra tellus in. Dignissim enim sit + amet venenatis urna cursus eget nunc. Mi proin sed libero enim. Ut sem viverra aliquet eget + sit amet. Integer enim neque volutpat ac tincidunt vitae. Tincidunt nunc pulvinar sapien et + ligula ullamcorper malesuada. Mauris rhoncus aenean vel elit scelerisque mauris + pellentesque. Lacus luctus accumsan tortor posuere ac ut consequat semper. Non pulvinar + neque laoreet suspendisse interdum consectetur libero id faucibus. Aliquam sem et tortor + consequat id. Cursus sit amet dictum sit amet justo donec. Donec adipiscing tristique risus + nec feugiat in fermentum posuere. +

-

Scelerisque in dictum non consectetur a. Elit pellentesque habitant morbi tristique senectus et. Nulla aliquet enim tortor at auctor urna nunc id. In ornare quam viverra orci. Auctor eu augue ut lectus arcu bibendum at varius vel. In cursus turpis massa tincidunt dui ut ornare lectus. Accumsan in nisl nisi scelerisque eu ultrices vitae auctor eu. A diam sollicitudin tempor id. Tellus mauris a diam maecenas sed enim ut sem. Pellentesque id nibh tortor id aliquet lectus proin. Fermentum et sollicitudin ac orci phasellus. Dolor morbi non arcu risus quis. Bibendum enim facilisis gravida neque. Tellus in metus vulputate eu scelerisque felis. Integer malesuada nunc vel risus commodo. Lacus laoreet non curabitur gravida arcu.

-
- - +

+ Diam donec adipiscing tristique risus nec. Amet nisl purus in mollis. Et malesuada fames ac + turpis egestas maecenas pharetra. Ante metus dictum at tempor commodo ullamcorper a. Dui id + ornare arcu odio ut sem nulla. Ut pharetra sit amet aliquam id diam maecenas. Scelerisque in + dictum non consectetur a erat nam at. In ante metus dictum at tempor. Eget nulla facilisi + etiam dignissim diam quis enim lobortis scelerisque. Euismod nisi porta lorem mollis aliquam + ut porttitor leo a. Malesuada proin libero nunc consequat interdum. Turpis egestas sed + tempus urna et pharetra pharetra massa massa. Quis blandit turpis cursus in hac habitasse. + Amet commodo nulla facilisi nullam vehicula ipsum a. +

+ +

+ Mauris ultrices eros in cursus turpis massa tincidunt. Aliquam ut porttitor leo a diam + sollicitudin. Curabitur vitae nunc sed velit. Cursus metus aliquam eleifend mi in nulla + posuere sollicitudin. Lectus nulla at volutpat diam ut. Nibh nisl condimentum id venenatis a + condimentum vitae sapien. Tincidunt id aliquet risus feugiat in ante metus. Elementum nibh + tellus molestie nunc non blandit massa enim. Ac tortor vitae purus faucibus ornare + suspendisse. Pellentesque sit amet porttitor eget. Commodo quis imperdiet massa tincidunt. + Nunc sed id semper risus in hendrerit gravida. Proin nibh nisl condimentum id venenatis a + condimentum. Tortor at risus viverra adipiscing at in. Pharetra massa massa ultricies mi + quis hendrerit dolor. Tempor id eu nisl nunc mi ipsum faucibus vitae. +

+

Tellus orci

+ +

+ Viverra mauris in aliquam sem. Viverra tellus in hac habitasse platea. Facilisi nullam + vehicula ipsum a arcu cursus. Nunc sed augue lacus viverra vitae congue eu. Pretium fusce id + velit ut tortor pretium viverra suspendisse. Eu scelerisque felis imperdiet proin. Tincidunt + arcu non sodales neque sodales ut etiam sit amet. Tellus at urna condimentum mattis + pellentesque. Congue nisi vitae suscipit tellus. Ut morbi tincidunt augue interdum. +

+ +

+ Scelerisque in dictum non consectetur a. Elit pellentesque habitant morbi tristique senectus + et. Nulla aliquet enim tortor at auctor urna nunc id. In ornare quam viverra orci. Auctor eu + augue ut lectus arcu bibendum at varius vel. In cursus turpis massa tincidunt dui ut ornare + lectus. Accumsan in nisl nisi scelerisque eu ultrices vitae auctor eu. A diam sollicitudin + tempor id. Tellus mauris a diam maecenas sed enim ut sem. Pellentesque id nibh tortor id + aliquet lectus proin. Fermentum et sollicitudin ac orci phasellus. Dolor morbi non arcu + risus quis. Bibendum enim facilisis gravida neque. Tellus in metus vulputate eu scelerisque + felis. Integer malesuada nunc vel risus commodo. Lacus laoreet non curabitur gravida arcu. +

+
+ + diff --git a/index.html b/index.html index c636520..8ab50df 100644 --- a/index.html +++ b/index.html @@ -1,37 +1,54 @@ - - + + - - CrinGraph - Frequency response graphs - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - - + + CrinGraph - Frequency response graphs + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + diff --git a/listAugment.js b/listAugment.js index 41248e7..3fa834f 100644 --- a/listAugment.js +++ b/listAugment.js @@ -1,358 +1,373 @@ function augmentInit() { - targetBody = document.querySelector('body'), - augmentStyle = document.createElement('style'), - augmentCss = ` - :root { - --icon-play-fill: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23231f20;fill-rule:evenodd;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M12,21a9,9,0,1,0-9-9A9,9,0,0,0,12,21ZM10.78,8l5.65,3.14a1,1,0,0,1,0,1.74L10.78,16A1.2,1.2,0,0,1,9,15V9A1.2,1.2,0,0,1,10.78,8Z'/%3E%3C/svg%3E"); - --icon-review-fill: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M2.87868 3.87868C2 4.75736 2 6.17157 2 9V15C2 17.8284 2 19.2426 2.87868 20.1213C3.75736 21 5.17157 21 8 21H16C18.8284 21 20.2426 21 21.1213 20.1213C22 19.2426 22 17.8284 22 15V9C22 6.17157 22 4.75736 21.1213 3.87868C20.2426 3 18.8284 3 16 3H8C5.17157 3 3.75736 3 2.87868 3.87868ZM16 8C16.5523 8 17 8.44772 17 9V17C17 17.5523 16.5523 18 16 18C15.4477 18 15 17.5523 15 17V9C15 8.44772 15.4477 8 16 8ZM9 11C9 10.4477 8.55228 10 8 10C7.44772 10 7 10.4477 7 11V17C7 17.5523 7.44772 18 8 18C8.55229 18 9 17.5523 9 17V11ZM13 13C13 12.4477 12.5523 12 12 12C11.4477 12 11 12.4477 11 13V17C11 17.5523 11.4477 18 12 18C12.5523 18 13 17.5523 13 17V13Z' fill='white'/%3E%3C/svg%3E%0A"); - --icon-star-empty: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:none;stroke:%23231f20;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M10.14,6.59c.79-2,1.18-2.94,1.86-2.94s1.07,1,1.86,2.94l0,.09c.45,1.11.67,1.66,1.12,2a4.44,4.44,0,0,0,2.24.5h.21c1.95.18,2.92.27,3.13.89s-.51,1.27-2,2.59l-.48.43c-.73.67-1.1,1-1.27,1.44a1.83,1.83,0,0,0-.08.25,4.49,4.49,0,0,0,.21,1.9l.07.3c.39,1.78.59,2.66.24,3.05a1,1,0,0,1-.48.29c-.49.14-1.2-.44-2.61-1.58a4.65,4.65,0,0,0-1.91-1.22,2.29,2.29,0,0,0-.64,0,4.65,4.65,0,0,0-1.91,1.22c-1.41,1.14-2.12,1.72-2.61,1.58A1,1,0,0,1,6.68,20c-.35-.39-.15-1.27.24-3.05l.07-.3a4.49,4.49,0,0,0,.21-1.9,1.83,1.83,0,0,0-.08-.25c-.17-.44-.54-.77-1.27-1.44l-.48-.43c-1.45-1.32-2.17-2-2-2.59s1.18-.71,3.13-.89h.21A4.44,4.44,0,0,0,9,8.68c.45-.34.67-.89,1.12-2Z'/%3E%3C/svg%3E"); - --icon-star-fill: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23231f20;stroke:%23231f20;stroke-width:2px;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M10.31,7.17c.64-1.6,1-2.4,1.48-2.51a1,1,0,0,1,.42,0c.52.11.84.91,1.48,2.51a4.57,4.57,0,0,0,.89,1.68,2.26,2.26,0,0,0,.31.23,4.52,4.52,0,0,0,1.88.37c1.66.15,2.5.22,2.75.7a.94.94,0,0,1,.11.32c.08.53-.53,1.09-1.76,2.2l-.34.31a4.38,4.38,0,0,0-1,1.11,2,2,0,0,0-.2.62,4.42,4.42,0,0,0,.2,1.5l.06.27c.3,1.36.45,2,.26,2.37a1,1,0,0,1-.82.51c-.38,0-.92-.42-2-1.3a4.55,4.55,0,0,0-1.46-1,2.05,2.05,0,0,0-1.1,0,4.55,4.55,0,0,0-1.46,1c-1.08.88-1.62,1.32-2,1.3a1,1,0,0,1-.82-.51c-.19-.33,0-1,.26-2.37l.06-.27a4.42,4.42,0,0,0,.2-1.5,2,2,0,0,0-.2-.62,4.38,4.38,0,0,0-1-1.11l-.34-.31C4.9,11.56,4.29,11,4.37,10.47a.94.94,0,0,1,.11-.32c.25-.48,1.09-.55,2.75-.7a4.52,4.52,0,0,0,1.88-.37,2.26,2.26,0,0,0,.31-.23A4.57,4.57,0,0,0,10.31,7.17Z'/%3E%3C/svg%3E"); - - } - - div.phone-item span { - position: relative; - z-index: 1; - } - + (targetBody = document.querySelector("body")), + (augmentStyle = document.createElement("style")), + (augmentCss = ` + :root { + --icon-play-fill: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23231f20;fill-rule:evenodd;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M12,21a9,9,0,1,0-9-9A9,9,0,0,0,12,21ZM10.78,8l5.65,3.14a1,1,0,0,1,0,1.74L10.78,16A1.2,1.2,0,0,1,9,15V9A1.2,1.2,0,0,1,10.78,8Z'/%3E%3C/svg%3E"); + --icon-review-fill: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M2.87868 3.87868C2 4.75736 2 6.17157 2 9V15C2 17.8284 2 19.2426 2.87868 20.1213C3.75736 21 5.17157 21 8 21H16C18.8284 21 20.2426 21 21.1213 20.1213C22 19.2426 22 17.8284 22 15V9C22 6.17157 22 4.75736 21.1213 3.87868C20.2426 3 18.8284 3 16 3H8C5.17157 3 3.75736 3 2.87868 3.87868ZM16 8C16.5523 8 17 8.44772 17 9V17C17 17.5523 16.5523 18 16 18C15.4477 18 15 17.5523 15 17V9C15 8.44772 15.4477 8 16 8ZM9 11C9 10.4477 8.55228 10 8 10C7.44772 10 7 10.4477 7 11V17C7 17.5523 7.44772 18 8 18C8.55229 18 9 17.5523 9 17V11ZM13 13C13 12.4477 12.5523 12 12 12C11.4477 12 11 12.4477 11 13V17C11 17.5523 11.4477 18 12 18C12.5523 18 13 17.5523 13 17V13Z' fill='white'/%3E%3C/svg%3E%0A"); + --icon-star-empty: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:none;stroke:%23231f20;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M10.14,6.59c.79-2,1.18-2.94,1.86-2.94s1.07,1,1.86,2.94l0,.09c.45,1.11.67,1.66,1.12,2a4.44,4.44,0,0,0,2.24.5h.21c1.95.18,2.92.27,3.13.89s-.51,1.27-2,2.59l-.48.43c-.73.67-1.1,1-1.27,1.44a1.83,1.83,0,0,0-.08.25,4.49,4.49,0,0,0,.21,1.9l.07.3c.39,1.78.59,2.66.24,3.05a1,1,0,0,1-.48.29c-.49.14-1.2-.44-2.61-1.58a4.65,4.65,0,0,0-1.91-1.22,2.29,2.29,0,0,0-.64,0,4.65,4.65,0,0,0-1.91,1.22c-1.41,1.14-2.12,1.72-2.61,1.58A1,1,0,0,1,6.68,20c-.35-.39-.15-1.27.24-3.05l.07-.3a4.49,4.49,0,0,0,.21-1.9,1.83,1.83,0,0,0-.08-.25c-.17-.44-.54-.77-1.27-1.44l-.48-.43c-1.45-1.32-2.17-2-2-2.59s1.18-.71,3.13-.89h.21A4.44,4.44,0,0,0,9,8.68c.45-.34.67-.89,1.12-2Z'/%3E%3C/svg%3E"); + --icon-star-fill: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23231f20;stroke:%23231f20;stroke-width:2px;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M10.31,7.17c.64-1.6,1-2.4,1.48-2.51a1,1,0,0,1,.42,0c.52.11.84.91,1.48,2.51a4.57,4.57,0,0,0,.89,1.68,2.26,2.26,0,0,0,.31.23,4.52,4.52,0,0,0,1.88.37c1.66.15,2.5.22,2.75.7a.94.94,0,0,1,.11.32c.08.53-.53,1.09-1.76,2.2l-.34.31a4.38,4.38,0,0,0-1,1.11,2,2,0,0,0-.2.62,4.42,4.42,0,0,0,.2,1.5l.06.27c.3,1.36.45,2,.26,2.37a1,1,0,0,1-.82.51c-.38,0-.92-.42-2-1.3a4.55,4.55,0,0,0-1.46-1,2.05,2.05,0,0,0-1.1,0,4.55,4.55,0,0,0-1.46,1c-1.08.88-1.62,1.32-2,1.3a1,1,0,0,1-.82-.51c-.19-.33,0-1,.26-2.37l.06-.27a4.42,4.42,0,0,0,.2-1.5,2,2,0,0,0-.2-.62,4.38,4.38,0,0,0-1-1.11l-.34-.31C4.9,11.56,4.29,11,4.37,10.47a.94.94,0,0,1,.11-.32c.25-.48,1.09-.55,2.75-.7a4.52,4.52,0,0,0,1.88-.37,2.26,2.26,0,0,0,.31-.23A4.57,4.57,0,0,0,10.31,7.17Z'/%3E%3C/svg%3E"); + } + + div.phone-item span { + position: relative; + z-index: 1; + } + + article.augment { + box-sizing: border-box; + + width: calc(350px - 94px); + margin-top: -6px; + margin-bottom: 6px; + border: 1px solid var(--font-color-primary); + border-top: 6px solid var(--font-color-primary); + border-top: none; + border-radius: 0 0 6px 6px; + + max-height: 0px; + overflow: hidden; + opacity: 0%; + + animation: augmentOut ease-out 0.1s 1 forwards; + } + + @media (max-width: 1000px) { article.augment { - box-sizing: border-box; - - width: calc(350px - 94px); - margin-top: -6px; - margin-bottom: 6px; - border: 1px solid var(--font-color-primary); - border-top: 6px solid var(--font-color-primary); - border-top: none; - border-radius: 0 0 6px 6px; - - max-height: 0px; - overflow: hidden; - opacity: 0%; - - animation: augmentOut ease-out 0.1s 1 forwards; - } - - @media (max-width: 1000px) { - article.augment { - width: calc(100vw - 94px); - } - } - - div.phone-item[style*="display: none"] + article.augment { - display: none; - } - - div.phone-item[style*="border"] + article.augment { - animation: augmentIn ease-out 0.1s 1 forwards; - } - - @keyframes augmentIn { - 0% { - max-height: 0px; - opacity: 0%; - } - 100% { - max-height: 200px; - opacity: 100%; - } - } - - @keyframes augmentOut { - 0% { - max-height: 200px; - opacity: 100%; - } - 100% { - max-height: 0px; - opacity: 0%; - } - } - - article.augment a { - color: var(--font-color-primary); - } - - div.augment-rank { - display: flex; - - padding: 11px 11px; - - background-color: var(--font-color-primary); - color: var(--font-color-secondary); - } - - div.augment-price { - margin-left: auto; - - font-size: 16px; - line-height: 1em; - } - - div.augment-review { - padding: 11px 11px; - } - - div.augment-review a { - display: flex; - font-size: 12px; - line-height: 18px; - } - - div.augment-review a:before { - content: ''; - display: block; - flex: 18px 0 0; - height: 18px; - margin: 0 8px 0 0; - background-color: currentColor; - mask: var(--icon-review-fill); - -webkit-mask: var(--icon-review-fill); - mask-size: 18px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 18px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; - } - - div.augment-review a.video:before { - mask: var(--icon-play-fill); - -webkit-mask: var(--icon-play-fill); - } - - div.augment-review + div.augment-shop { - border-top: 1px solid var(--background-color-contrast) - } - - div.augment-shop { - padding: 11px 11px; - } - - div.augment-shop a { - display: flex; - font-size: 12px; - line-height: 18px; - } - - div.augment-shop a:before { - content: ''; - display: block; - flex: 18px 0 0; - height: 18px; - margin: 0 8px 0 0; - background-color: currentColor; - mask: var(--icon-new-tab); - -webkit-mask: var(--icon-new-tab); - mask-size: 14px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 14px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; - } - - div.augment-score-unknown { - font-size: 16px; - } - - div.augment-stars { - display: flex; - align-items: flex-end; - } - - div.augment-stars span { - box-sizing: border-box; - display: block; - width: 18px; - height: 18px; - - background-color: currentColor; - - mask: var(--icon-star-empty); - -webkit-mask: var(--icon-star-empty); - mask-size: 18px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 18px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; - } - - div.augment-stars[data-score="5"] span { - mask: var(--icon-star-fill); - -webkit-mask: var(--icon-star-fill); - } - - div.augment-stars[data-score="4"] span:nth-last-child(1n + 2) { - mask: var(--icon-star-fill); - -webkit-mask: var(--icon-star-fill); - } - - div.augment-stars[data-score="3"] span:nth-last-child(1n + 3) { - mask: var(--icon-star-fill); - -webkit-mask: var(--icon-star-fill); - } - - div.augment-stars[data-score="2"] span:nth-last-child(1n + 4) { - mask: var(--icon-star-fill); - -webkit-mask: var(--icon-star-fill); - } - - div.augment-stars[data-score="1"] span:nth-last-child(1n + 5) { - mask: var(--icon-star-fill); - -webkit-mask: var(--icon-star-fill); - } - - div.augment-stars[data-score="0"] span { - opacity: 0.3; - } - - div.augment-stars[data-score="0"]:after { - content: 'TBD'; - - margin-left: 6px; - - font-size: 10px; - line-height: 18px; - - opacity: 0.3; - } - - /* - 5-stars */ - - div.scroll div.phone-item[style*="border"][data-score="5"] { - color: var(--font-color-primary); - - border: 1px solid #ffeb40 !important; - background-color: #ffeb40 !important; - background: linear-gradient(-90deg, #ffeb40, #fff7b2) !important; - } - - div.scroll div.phone-item[style*="border"][data-score="5"] span { - padding: 11px 11px; - } - - div.scroll div.phone-item[style*="border"] { - background-color: var(--font-color-primary) !important; - } + width: calc(100vw - 94px); + } + } + + div.phone-item[style*="display: none"] + article.augment { + display: none; + } + + div.phone-item[style*="border"] + article.augment { + animation: augmentIn ease-out 0.1s 1 forwards; + } + + @keyframes augmentIn { + 0% { + max-height: 0px; + opacity: 0%; + } + 100% { + max-height: 200px; + opacity: 100%; + } + } + + @keyframes augmentOut { + 0% { + max-height: 200px; + opacity: 100%; + } + 100% { + max-height: 0px; + opacity: 0%; + } + } + + article.augment a { + color: var(--font-color-primary); + } + + div.augment-rank { + display: flex; + + padding: 11px 11px; + + background-color: var(--font-color-primary); + color: var(--font-color-secondary); + } + + div.augment-price { + margin-left: auto; + + font-size: 16px; + line-height: 1em; + } + + div.augment-review { + padding: 11px 11px; + } + + div.augment-review a { + display: flex; + font-size: 12px; + line-height: 18px; + } + + div.augment-review a:before { + content: ''; + display: block; + flex: 18px 0 0; + height: 18px; + margin: 0 8px 0 0; + background-color: currentColor; + mask: var(--icon-review-fill); + -webkit-mask: var(--icon-review-fill); + mask-size: 18px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 18px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + } + + div.augment-review a.video:before { + mask: var(--icon-play-fill); + -webkit-mask: var(--icon-play-fill); + } + + div.augment-review + div.augment-shop { + border-top: 1px solid var(--background-color-contrast) + } + + div.augment-shop { + padding: 11px 11px; + } + + div.augment-shop a { + display: flex; + font-size: 12px; + line-height: 18px; + } + + div.augment-shop a:before { + content: ''; + display: block; + flex: 18px 0 0; + height: 18px; + margin: 0 8px 0 0; + background-color: currentColor; + mask: var(--icon-new-tab); + -webkit-mask: var(--icon-new-tab); + mask-size: 14px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 14px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + } + + div.augment-score-unknown { + font-size: 16px; + } + + div.augment-stars { + display: flex; + align-items: flex-end; + } + + div.augment-stars span { + box-sizing: border-box; + display: block; + width: 18px; + height: 18px; + + background-color: currentColor; + + mask: var(--icon-star-empty); + -webkit-mask: var(--icon-star-empty); + mask-size: 18px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 18px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + } + + div.augment-stars[data-score="5"] span { + mask: var(--icon-star-fill); + -webkit-mask: var(--icon-star-fill); + } + + div.augment-stars[data-score="4"] span:nth-last-child(1n + 2) { + mask: var(--icon-star-fill); + -webkit-mask: var(--icon-star-fill); + } + + div.augment-stars[data-score="3"] span:nth-last-child(1n + 3) { + mask: var(--icon-star-fill); + -webkit-mask: var(--icon-star-fill); + } + + div.augment-stars[data-score="2"] span:nth-last-child(1n + 4) { + mask: var(--icon-star-fill); + -webkit-mask: var(--icon-star-fill); + } + + div.augment-stars[data-score="1"] span:nth-last-child(1n + 5) { + mask: var(--icon-star-fill); + -webkit-mask: var(--icon-star-fill); + } + + div.augment-stars[data-score="0"] span { + opacity: 0.3; + } + + div.augment-stars[data-score="0"]:after { + content: 'TBD'; + + margin-left: 6px; + + font-size: 10px; + line-height: 18px; + + opacity: 0.3; + } + + /* + 5-stars */ + + div.scroll div.phone-item[style*="border"][data-score="5"] { + color: var(--font-color-primary); + + border: 1px solid #ffeb40 !important; + background-color: #ffeb40 !important; + background: linear-gradient(-90deg, #ffeb40, #fff7b2) !important; + } + + div.scroll div.phone-item[style*="border"][data-score="5"] span { + padding: 11px 11px; + } + + div.scroll div.phone-item[style*="border"] { + background-color: var(--font-color-primary) !important; + } + + div.scroll div.phone-item[style*="border"][data-score="5"] div.phone-item-add span.remove:before { + background-color: var(--font-color-primary); + } + + div.scroll div[style*="border"].phone-item[data-score="5"] + article { + border-color: #ffeb40; + } + + div.scroll div[style*="border"].phone-item[data-score="5"] + article div.augment-rank { + color: var(--font-color-primary); + + background-color: #ffeb40 !important; + background: linear-gradient(-90deg, #ffeb40, #fff7b2) !important; + } + `), + (isUs = window.navigator.language.split("-").pop() === "US" ? 1 : 0); + + augmentStyle.textContent = augmentCss; + targetBody.append(augmentStyle); +} +augmentInit(); - div.scroll div.phone-item[style*="border"][data-score="5"] div.phone-item-add span.remove:before { - background-color: var(--font-color-primary); - } +function augmentList(phone) { + let phoneName = phone.fullName, + phoneListItem = document.querySelector('div[name="' + phoneName + '"]'), + phoneListItemAugmented = phoneListItem.getAttribute("data-augment"), + reviewScore = phone.reviewScore + ? phone.reviewScore.length === 1 && typeof parseInt(phone.reviewScore) === "number" + ? parseInt(phone.reviewScore) + : phone.reviewScore + : false, + reviewStars = !reviewScore.length && reviewScore >= 0 && reviewScore <= 5 ? true : false, + reviewLink = phone.reviewLink, + reviewLinkLabel = reviewLink ? reviewLink.split("http.").pop().split("/").shift() : false, + reviewLinkVideo = reviewLink ? (reviewLink.includes("youtube") ? 1 : 0) : 0, + shopLink = phone.shopLink, + shopLinkAmazon = shopLink + ? shopLink.indexOf("amazon") > 0 + ? true + : shopLink.indexOf("amzn") > 0 + ? true + : false + : false, + shopLinkAli = shopLink ? (shopLink.indexOf("aliexpress") > 0 ? true : false) : false, + shopLinkLabel = shopLink + ? shopLinkAmazon + ? "Amazon" + : shopLinkAli + ? "AliExpress" + : shopLink.replace("www.", "").split("://").pop().split("/").shift() + : false, + price = phone.price; + + if (!phoneListItemAugmented) { + let agumentsContainer = document.createElement("article"), + augmentsRow1 = document.createElement("div"), + augmentsRow1Col1 = document.createElement("div"), + augmentsRow1Col2 = document.createElement("div"), + augmentsStar1 = document.createElement("span"), + augmentsStar2 = document.createElement("span"), + augmentsStar3 = document.createElement("span"), + augmentsStar4 = document.createElement("span"), + augmentsStar5 = document.createElement("span"), + augmentsRow2 = document.createElement("div"), + augmentsRow3 = document.createElement("div"), + augmentsReviewLink = document.createElement("a"), + augmentsShopLink = document.createElement("a"); + + phoneListItem.setAttribute("data-augment", "1"); + + agumentsContainer.className = "augment"; + + augmentsRow1.append(augmentsRow1Col1); + augmentsRow1.append(augmentsRow1Col2); + augmentsRow1Col2.textContent = price; + augmentsRow1Col2.className = "augment-price"; + + if (typeof reviewScore === "number" && reviewStars) { + agumentsContainer.append(augmentsRow1); + augmentsRow1.className = "augment-rank"; + + augmentsRow1Col1.setAttribute("data-score", reviewScore); + augmentsRow1Col1.append(augmentsStar1); + augmentsRow1Col1.append(augmentsStar2); + augmentsRow1Col1.append(augmentsStar3); + augmentsRow1Col1.append(augmentsStar4); + augmentsRow1Col1.append(augmentsStar5); + augmentsRow1Col1.className = "augment-stars"; + } else if (reviewScore && !reviewStars) { + agumentsContainer.append(augmentsRow1); + augmentsRow1.className = "augment-rank"; + + augmentsRow1Col1.className = "augment-score augment-score-unknown"; + augmentsRow1Col1.textContent = reviewScore; + } - div.scroll div[style*="border"].phone-item[data-score="5"] + article { - border-color: #ffeb40; - } + if (reviewLink) { + augmentsRow2.append(augmentsReviewLink); + augmentsReviewLink.setAttribute("target", "_blank"); + augmentsRow2.className = "augment-review"; + + if (reviewLinkVideo) { + augmentsReviewLink.classList.add("video"); + } + augmentsReviewLink.setAttribute("href", reviewLink); + augmentsReviewLink.textContent = "Review"; + if (analyticsEnabled) { + augmentsReviewLink.addEventListener("click", function () { + pushPhoneTag("clicked_review", phone); + }); + } + + agumentsContainer.append(augmentsRow2); + } - div.scroll div[style*="border"].phone-item[data-score="5"] + article div.augment-rank { - color: var(--font-color-primary); + if (shopLink) { + augmentsRow3.append(augmentsShopLink); + augmentsRow3.className = "augment-shop"; + augmentsShopLink.setAttribute("target", "_blank"); - background-color: #ffeb40 !important; - background: linear-gradient(-90deg, #ffeb40, #fff7b2) !important; - } - `, - isUs = window.navigator.language.split('-').pop() === 'US' ? 1 : 0; - - augmentStyle.textContent = augmentCss; - targetBody.append(augmentStyle); -} -augmentInit(); + augmentsShopLink.setAttribute("href", shopLink); + augmentsShopLink.textContent = shopLinkLabel; + if (analyticsEnabled) { + augmentsShopLink.addEventListener("click", function () { + pushPhoneTag("clicked_store", phone); + }); + } -function augmentList(phone) { - let phoneName = phone.fullName, - phoneListItem = document.querySelector('div[name="'+ phoneName +'"]'), - phoneListItemAugmented = phoneListItem.getAttribute('data-augment'), - reviewScore = phone.reviewScore ? phone.reviewScore.length === 1 && typeof parseInt(phone.reviewScore) === 'number' ? parseInt(phone.reviewScore) : phone.reviewScore : false, - reviewStars = !reviewScore.length && reviewScore >= 0 && reviewScore <= 5 ? true : false, - reviewLink = phone.reviewLink, - reviewLinkLabel = reviewLink ? reviewLink.split('http.').pop().split('/').shift() : false, - reviewLinkVideo = reviewLink ? reviewLink.includes('youtube') ? 1 : 0 : 0, - shopLink = phone.shopLink, - shopLinkAmazon = shopLink ? shopLink.indexOf('amazon') > 0 ? true : shopLink.indexOf('amzn') > 0 ? true : false : false, - shopLinkAli = shopLink ? shopLink.indexOf('aliexpress') > 0 ? true : false : false, - shopLinkLabel = shopLink ? shopLinkAmazon ? 'Amazon' : shopLinkAli ? 'AliExpress' : shopLink.replace('www.','').split('://').pop().split('/').shift() : false, - price = phone.price; - - if (!phoneListItemAugmented) { - let agumentsContainer = document.createElement('article'), - augmentsRow1 = document.createElement('div'), - augmentsRow1Col1 = document.createElement('div'), - augmentsRow1Col2 = document.createElement('div'), - augmentsStar1 = document.createElement('span'), - augmentsStar2 = document.createElement('span'), - augmentsStar3 = document.createElement('span'), - augmentsStar4 = document.createElement('span'), - augmentsStar5 = document.createElement('span'), - augmentsRow2 = document.createElement('div'), - augmentsRow3 = document.createElement('div'), - augmentsReviewLink = document.createElement('a'), - augmentsShopLink = document.createElement('a'); - - phoneListItem.setAttribute('data-augment', '1'); - - agumentsContainer.className = "augment"; - - augmentsRow1.append(augmentsRow1Col1); - augmentsRow1.append(augmentsRow1Col2); - augmentsRow1Col2.textContent = price; - augmentsRow1Col2.className = "augment-price"; - - if (typeof reviewScore === 'number' && reviewStars) { - agumentsContainer.append(augmentsRow1); - augmentsRow1.className = "augment-rank"; - - augmentsRow1Col1.setAttribute('data-score', reviewScore); - augmentsRow1Col1.append(augmentsStar1); - augmentsRow1Col1.append(augmentsStar2); - augmentsRow1Col1.append(augmentsStar3); - augmentsRow1Col1.append(augmentsStar4); - augmentsRow1Col1.append(augmentsStar5); - augmentsRow1Col1.className = "augment-stars"; - } else if (reviewScore && !reviewStars) { - agumentsContainer.append(augmentsRow1); - augmentsRow1.className = "augment-rank"; - - augmentsRow1Col1.className = "augment-score augment-score-unknown"; - augmentsRow1Col1.textContent = reviewScore; - } - - if (reviewLink) { - augmentsRow2.append(augmentsReviewLink); - augmentsReviewLink.setAttribute('target', '_blank'); - augmentsRow2.className = "augment-review"; - - if (reviewLinkVideo) { - augmentsReviewLink.classList.add('video'); - } - augmentsReviewLink.setAttribute('href', reviewLink); - augmentsReviewLink.textContent = 'Review'; - if (analyticsEnabled) { - augmentsReviewLink.addEventListener('click', function() { - pushPhoneTag("clicked_review", phone); - }); - } - - agumentsContainer.append(augmentsRow2); - } - - if (shopLink) { - augmentsRow3.append(augmentsShopLink); - augmentsRow3.className = "augment-shop"; - augmentsShopLink.setAttribute('target', '_blank'); - - augmentsShopLink.setAttribute('href', shopLink); - augmentsShopLink.textContent = shopLinkLabel; - if (analyticsEnabled) { - augmentsShopLink.addEventListener('click', function() { - pushPhoneTag("clicked_store", phone); - }); - } - - agumentsContainer.append(augmentsRow3); - } - - phoneListItem.parentNode.insertBefore(agumentsContainer, phoneListItem.nextSibling); + agumentsContainer.append(augmentsRow3); } + + phoneListItem.parentNode.insertBefore(agumentsContainer, phoneListItem.nextSibling); + } } diff --git a/package.json b/package.json new file mode 100644 index 0000000..d2d86c6 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "squiglink-lab", + "type": "module", + "devDependencies": { + "oxlint": "^0.16.9", + "prettier": "^3.5.3" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..af96431 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,134 @@ +lockfileVersion: "9.0" + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + .: + devDependencies: + oxlint: + specifier: ^0.16.9 + version: 0.16.9 + prettier: + specifier: ^3.5.3 + version: 3.5.3 + +packages: + "@oxlint/darwin-arm64@0.16.9": + resolution: + { + integrity: sha512-s8gPacumFNuDQcl0dCRLI0+efbiQrOF984brGrW1i2buJtaMzjYiunaU5TSrbHgnb/omvpFnG4l9g+YHOK/s0A==, + } + cpu: [arm64] + os: [darwin] + + "@oxlint/darwin-x64@0.16.9": + resolution: + { + integrity: sha512-QFue9yhfRU+fkbsOmDzCCwDafPR6fnaP/M+Rocp/MMDQ7qvjbB2sj0/xxp/CcvL7aLtFklMeXPR0hCfW3LyrSw==, + } + cpu: [x64] + os: [darwin] + + "@oxlint/linux-arm64-gnu@0.16.9": + resolution: + { + integrity: sha512-qP/wdlgqLuiW9WDkAsyMN85wQ3nqAQynjRD+1II1QO0yI9N1ZHD6LF9P5fXAqY0eJwcf3emluQMoaeveewtiCg==, + } + cpu: [arm64] + os: [linux] + + "@oxlint/linux-arm64-musl@0.16.9": + resolution: + { + integrity: sha512-486wn1MIqP4hkHTnuWedTb16X6Zs3ImmmMxqzfYlcemf9kODM6yNlxal6wGvkm7SGRPYrsB/P9S5wgpzmLzKrw==, + } + cpu: [arm64] + os: [linux] + + "@oxlint/linux-x64-gnu@0.16.9": + resolution: + { + integrity: sha512-d20zqy4Mimz9CxjIEJdGd6jtyyhpSryff95gNJSTvh/EA4NqvjjlbjxuHt3clNjglRwJWE3kgmUCw9U9oWFcWA==, + } + cpu: [x64] + os: [linux] + + "@oxlint/linux-x64-musl@0.16.9": + resolution: + { + integrity: sha512-mZLshz99773RMa77YhtsgWbqE7JY4xnYSDecDy+ZkafRb0acqz1Ujiq2l4cE+HnJOGVOMaOpzG0UoQ3ZNkXNAA==, + } + cpu: [x64] + os: [linux] + + "@oxlint/win32-arm64@0.16.9": + resolution: + { + integrity: sha512-Zp9+0CfTb7ebgvRwDO2F6NVgRtRmxWMdBnrbMRdVbKY6CCT2vjLAIILwBf5AsNLdLQC7FbXAEivSKbRX0UPyJA==, + } + cpu: [arm64] + os: [win32] + + "@oxlint/win32-x64@0.16.9": + resolution: + { + integrity: sha512-6a507bALmDNFdvEbJzu9ybajryBHo+6nMWPNyu/mBguCqmconoBbQXftELd2VG/0ecxBmBcMKAQW6aONGUVc3w==, + } + cpu: [x64] + os: [win32] + + oxlint@0.16.9: + resolution: + { + integrity: sha512-YMGu177AURJxdCq45/Yw6Q+uDh9ZfU++GuLYhUz+DfIGdHpAqVlBI9lCqm2HkLc6qO8ySYZ+8ljsWHLQA8F+EQ==, + } + engines: { node: ">=8.*" } + hasBin: true + + prettier@3.5.3: + resolution: + { + integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==, + } + engines: { node: ">=14" } + hasBin: true + +snapshots: + "@oxlint/darwin-arm64@0.16.9": + optional: true + + "@oxlint/darwin-x64@0.16.9": + optional: true + + "@oxlint/linux-arm64-gnu@0.16.9": + optional: true + + "@oxlint/linux-arm64-musl@0.16.9": + optional: true + + "@oxlint/linux-x64-gnu@0.16.9": + optional: true + + "@oxlint/linux-x64-musl@0.16.9": + optional: true + + "@oxlint/win32-arm64@0.16.9": + optional: true + + "@oxlint/win32-x64@0.16.9": + optional: true + + oxlint@0.16.9: + optionalDependencies: + "@oxlint/darwin-arm64": 0.16.9 + "@oxlint/darwin-x64": 0.16.9 + "@oxlint/linux-arm64-gnu": 0.16.9 + "@oxlint/linux-arm64-musl": 0.16.9 + "@oxlint/linux-x64-gnu": 0.16.9 + "@oxlint/linux-x64-musl": 0.16.9 + "@oxlint/win32-arm64": 0.16.9 + "@oxlint/win32-x64": 0.16.9 + + prettier@3.5.3: {} diff --git a/saveSvgAsPng.js b/saveSvgAsPng.js index fb44904..ccd0a1b 100644 --- a/saveSvgAsPng.js +++ b/saveSvgAsPng.js @@ -22,102 +22,110 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -(function() { - const out$ = typeof exports != 'undefined' && exports || typeof define != 'undefined' && {} || this || window; - if (typeof define !== 'undefined') define('save-svg-as-png', [], () => out$); +(function () { + const out$ = + (typeof exports != "undefined" && exports) || + (typeof define != "undefined" && {}) || + this || + window; + if (typeof define !== "undefined") define("save-svg-as-png", [], () => out$); out$.default = out$; - const xmlNs = 'http://www.w3.org/2000/xmlns/'; - const xhtmlNs = 'http://www.w3.org/1999/xhtml'; - const svgNs = 'http://www.w3.org/2000/svg'; - const doctype = ']>'; + const xmlNs = "http://www.w3.org/2000/xmlns/"; + const xhtmlNs = "http://www.w3.org/1999/xhtml"; + const svgNs = "http://www.w3.org/2000/svg"; + const doctype = + ']>'; const urlRegex = /url\(["']?(.+?)["']?\)/; const fontFormats = { - woff2: 'font/woff2', - woff: 'font/woff', - otf: 'application/x-font-opentype', - ttf: 'application/x-font-ttf', - eot: 'application/vnd.ms-fontobject', - sfnt: 'application/font-sfnt', - svg: 'image/svg+xml' + woff2: "font/woff2", + woff: "font/woff", + otf: "application/x-font-opentype", + ttf: "application/x-font-ttf", + eot: "application/vnd.ms-fontobject", + sfnt: "application/font-sfnt", + svg: "image/svg+xml", }; - const isElement = obj => obj instanceof HTMLElement || obj instanceof SVGElement; - const requireDomNode = el => { + const isElement = (obj) => obj instanceof HTMLElement || obj instanceof SVGElement; + const requireDomNode = (el) => { if (!isElement(el)) throw new Error(`an HTMLElement or SVGElement is required; got ${el}`); }; - const requireDomNodePromise = el => + const requireDomNodePromise = (el) => new Promise((resolve, reject) => { - if (isElement(el)) resolve(el) + if (isElement(el)) resolve(el); else reject(new Error(`an HTMLElement or SVGElement is required; got ${el}`)); - }) - const isExternal = url => url && url.lastIndexOf('http',0) === 0 && url.lastIndexOf(window.location.host) === -1; + }); + const isExternal = (url) => + url && url.lastIndexOf("http", 0) === 0 && url.lastIndexOf(window.location.host) === -1; - const getFontMimeTypeFromUrl = fontUrl => { + const getFontMimeTypeFromUrl = (fontUrl) => { const formats = Object.keys(fontFormats) - .filter(extension => fontUrl.indexOf(`.${extension}`) > 0) - .map(extension => fontFormats[extension]); + .filter((extension) => fontUrl.indexOf(`.${extension}`) > 0) + .map((extension) => fontFormats[extension]); if (formats) return formats[0]; console.error(`Unknown font format for ${fontUrl}. Fonts may not be working correctly.`); - return 'application/octet-stream'; + return "application/octet-stream"; }; - const arrayBufferToBase64 = buffer => { - let binary = ''; + const arrayBufferToBase64 = (buffer) => { + let binary = ""; const bytes = new Uint8Array(buffer); for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]); return window.btoa(binary); - } + }; const getDimension = (el, clone, dim) => { const v = (el.viewBox && el.viewBox.baseVal && el.viewBox.baseVal[dim]) || - (clone.getAttribute(dim) !== null && !clone.getAttribute(dim).match(/%$/) && parseInt(clone.getAttribute(dim))) || + (clone.getAttribute(dim) !== null && + !clone.getAttribute(dim).match(/%$/) && + parseInt(clone.getAttribute(dim))) || el.getBoundingClientRect()[dim] || parseInt(clone.style[dim]) || parseInt(window.getComputedStyle(el).getPropertyValue(dim)); - return typeof v === 'undefined' || v === null || isNaN(parseFloat(v)) ? 0 : v; + return typeof v === "undefined" || v === null || isNaN(parseFloat(v)) ? 0 : v; }; const getDimensions = (el, clone, width, height) => { - if (el.tagName === 'svg') return { - width: width || getDimension(el, clone, 'width'), - height: height || getDimension(el, clone, 'height') - }; + if (el.tagName === "svg") + return { + width: width || getDimension(el, clone, "width"), + height: height || getDimension(el, clone, "height"), + }; else if (el.getBBox) { - const {x, y, width, height} = el.getBBox(); + const { x, y, width, height } = el.getBBox(); return { width: x + width, - height: y + height + height: y + height, }; } }; - const reEncode = data => + const reEncode = (data) => decodeURIComponent( - encodeURIComponent(data) - .replace(/%([0-9A-F]{2})/g, (match, p1) => { - const c = String.fromCharCode(`0x${p1}`); - return c === '%' ? '%25' : c; - }) + encodeURIComponent(data).replace(/%([0-9A-F]{2})/g, (match, p1) => { + const c = String.fromCharCode(`0x${p1}`); + return c === "%" ? "%25" : c; + }), ); - const uriToBlob = uri => { - const byteString = window.atob(uri.split(',')[1]); - const mimeString = uri.split(',')[0].split(':')[1].split(';')[0] + const uriToBlob = (uri) => { + const byteString = window.atob(uri.split(",")[1]); + const mimeString = uri.split(",")[0].split(":")[1].split(";")[0]; const buffer = new ArrayBuffer(byteString.length); const intArray = new Uint8Array(buffer); for (let i = 0; i < byteString.length; i++) { intArray[i] = byteString.charCodeAt(i); } - return new Blob([buffer], {type: mimeString}); + return new Blob([buffer], { type: mimeString }); }; const query = (el, selector) => { if (!selector) return; try { - return el.querySelector(selector) || el.parentNode && el.parentNode.querySelector(selector); - } catch(err) { + return el.querySelector(selector) || (el.parentNode && el.parentNode.querySelector(selector)); + } catch (err) { console.warn(`Invalid CSS selector "${selector}"`, err); } }; @@ -128,105 +136,115 @@ THE SOFTWARE. // src: local('Abel'), url(https://fonts.gstatic.com/s/abel/v6/UzN-iejR1VoXU2Oc-7LsbvesZW2xOQ-xsNqO47m55DA.woff2); // } const match = rule.cssText.match(urlRegex); - const url = (match && match[1]) || ''; - if (!url || url.match(/^data:/) || url === 'about:blank') return; - const fullUrl = - url.startsWith('../') ? `${href}/../${url}` - : url.startsWith('./') ? `${href}/.${url}` - : url; + const url = (match && match[1]) || ""; + if (!url || url.match(/^data:/) || url === "about:blank") return; + const fullUrl = url.startsWith("../") + ? `${href}/../${url}` + : url.startsWith("./") + ? `${href}/.${url}` + : url; return { text: rule.cssText, format: getFontMimeTypeFromUrl(fullUrl), - url: fullUrl + url: fullUrl, }; }; - const inlineImages = el => Promise.all( - Array.from(el.querySelectorAll('image')).map(image => { - let href = image.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || image.getAttribute('href'); - // Removed external handling in CrinGraph since it will fail cross-origin stuff anyway - if (!href || isExternal(href)) return Promise.resolve(null); - return new Promise((resolve, reject) => { - const canvas = document.createElement('canvas'); - const img = new Image(); - img.crossOrigin = 'anonymous'; - img.src = href; - img.onerror = () => reject(new Error(`Could not load ${href}`)); - img.onload = () => { - canvas.width = img.width; - canvas.height = img.height; - canvas.getContext('2d').drawImage(img, 0, 0); - image.setAttributeNS('http://www.w3.org/1999/xlink', 'href', canvas.toDataURL('image/png')); - resolve(true); - }; - }); - }) - ); + const inlineImages = (el) => + Promise.all( + Array.from(el.querySelectorAll("image")).map((image) => { + let href = + image.getAttributeNS("http://www.w3.org/1999/xlink", "href") || + image.getAttribute("href"); + // Removed external handling in CrinGraph since it will fail cross-origin stuff anyway + if (!href || isExternal(href)) return Promise.resolve(null); + return new Promise((resolve, reject) => { + const canvas = document.createElement("canvas"); + const img = new Image(); + img.crossOrigin = "anonymous"; + img.src = href; + img.onerror = () => reject(new Error(`Could not load ${href}`)); + img.onload = () => { + canvas.width = img.width; + canvas.height = img.height; + canvas.getContext("2d").drawImage(img, 0, 0); + image.setAttributeNS( + "http://www.w3.org/1999/xlink", + "href", + canvas.toDataURL("image/png"), + ); + resolve(true); + }; + }); + }), + ); const cachedFonts = {}; - const inlineFonts = fonts => Promise.all( - fonts.map(font => - new Promise((resolve, reject) => { - if (cachedFonts[font.url]) return resolve(cachedFonts[font.url]); - - const req = new XMLHttpRequest(); - req.addEventListener('load', () => { - // TODO: it may also be worth it to wait until fonts are fully loaded before - // attempting to rasterize them. (e.g. use https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet) - const fontInBase64 = arrayBufferToBase64(req.response); - const fontUri = font.text.replace(urlRegex, `url("data:${font.format};base64,${fontInBase64}")`)+'\n'; - cachedFonts[font.url] = fontUri; - resolve(fontUri); - }); - req.addEventListener('error', e => { - console.warn(`Failed to load font from: ${font.url}`, e); - cachedFonts[font.url] = null; - resolve(null); - }); - req.addEventListener('abort', e => { - console.warn(`Aborted loading font from: ${font.url}`, e); - resolve(null); - }); - req.open('GET', font.url); - req.responseType = 'arraybuffer'; - req.send(); - }) - ) - ).then(fontCss => fontCss.filter(x => x).join('')); + const inlineFonts = (fonts) => + Promise.all( + fonts.map( + (font) => + new Promise((resolve, reject) => { + if (cachedFonts[font.url]) return resolve(cachedFonts[font.url]); + + const req = new XMLHttpRequest(); + req.addEventListener("load", () => { + // TODO: it may also be worth it to wait until fonts are fully loaded before + // attempting to rasterize them. (e.g. use https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet) + const fontInBase64 = arrayBufferToBase64(req.response); + const fontUri = + font.text.replace(urlRegex, `url("data:${font.format};base64,${fontInBase64}")`) + + "\n"; + cachedFonts[font.url] = fontUri; + resolve(fontUri); + }); + req.addEventListener("error", (e) => { + console.warn(`Failed to load font from: ${font.url}`, e); + cachedFonts[font.url] = null; + resolve(null); + }); + req.addEventListener("abort", (e) => { + console.warn(`Aborted loading font from: ${font.url}`, e); + resolve(null); + }); + req.open("GET", font.url); + req.responseType = "arraybuffer"; + req.send(); + }), + ), + ).then((fontCss) => fontCss.filter((x) => x).join("")); let cachedRules = null; const styleSheetRules = () => { if (cachedRules) return cachedRules; - return cachedRules = Array.from(document.styleSheets).map(sheet => { + return (cachedRules = Array.from(document.styleSheets).map((sheet) => { try { - return {rules: sheet.cssRules, href: sheet.href}; + return { rules: sheet.cssRules, href: sheet.href }; } catch (e) { console.warn(`Stylesheet could not be loaded: ${sheet.href}`, e); return {}; } - }); + })); }; const inlineCss = (el, options) => { - const { - selectorRemap, - modifyStyle, - modifyCss, - fonts - } = options || {}; - const generateCss = modifyCss || ((selector, properties) => { - const sel = selectorRemap ? selectorRemap(selector) : selector; - const props = modifyStyle ? modifyStyle(properties) : properties; - return `${sel}{${props}}\n`; - }); + const { selectorRemap, modifyStyle, modifyCss, fonts } = options || {}; + const generateCss = + modifyCss || + ((selector, properties) => { + const sel = selectorRemap ? selectorRemap(selector) : selector; + const props = modifyStyle ? modifyStyle(properties) : properties; + return `${sel}{${props}}\n`; + }); const css = []; - const detectFonts = typeof fonts === 'undefined'; + const detectFonts = typeof fonts === "undefined"; const fontList = fonts || []; - styleSheetRules().forEach(({rules, href}) => { + styleSheetRules().forEach(({ rules, href }) => { if (!rules) return; - Array.from(rules).forEach(rule => { - if (typeof rule.style != 'undefined') { - if (query(el, rule.selectorText)) css.push(generateCss(rule.selectorText, rule.style.cssText)); + Array.from(rules).forEach((rule) => { + if (typeof rule.style != "undefined") { + if (query(el, rule.selectorText)) + css.push(generateCss(rule.selectorText, rule.style.cssText)); else if (detectFonts && rule.cssText.match(/^@font-face/)) { const font = detectCssFont(rule, href); if (font) fontList.push(font); @@ -235,105 +253,104 @@ THE SOFTWARE. }); }); - return inlineFonts(fontList).then(fontCss => css.join('\n') + fontCss); + return inlineFonts(fontList).then((fontCss) => css.join("\n") + fontCss); }; const downloadOptions = () => { - if (!navigator.msSaveOrOpenBlob && !('download' in document.createElement('a'))) { - return {popup: window.open()}; + if (!navigator.msSaveOrOpenBlob && !("download" in document.createElement("a"))) { + return { popup: window.open() }; } }; out$.prepareSvg = (el, options, done) => { requireDomNode(el); - const { - left = 0, - top = 0, - width: w, - height: h, - scale = 1, - responsive = false, - } = options || {}; + const { left = 0, top = 0, width: w, height: h, scale = 1, responsive = false } = options || {}; return inlineImages(el).then(() => { let clone = el.cloneNode(true); clone.style.backgroundColor = (options || {}).backgroundColor || el.style.backgroundColor; - const {width, height} = getDimensions(el, clone, w, h); + const { width, height } = getDimensions(el, clone, w, h); - if (el.tagName !== 'svg') { + if (el.tagName !== "svg") { if (el.getBBox) { - if (clone.getAttribute('transform') != null) { - clone.setAttribute('transform', clone.getAttribute('transform').replace(/translate\(.*?\)/, '')); + if (clone.getAttribute("transform") != null) { + clone.setAttribute( + "transform", + clone.getAttribute("transform").replace(/translate\(.*?\)/, ""), + ); } - const svg = document.createElementNS('http://www.w3.org/2000/svg','svg'); + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.appendChild(clone); clone = svg; } else { - console.error('Attempted to render non-SVG element', el); + console.error("Attempted to render non-SVG element", el); return; } } - clone.setAttribute('version', '1.1'); - clone.setAttribute('viewBox', [left, top, width, height].join(' ')); - if (!clone.getAttribute('xmlns')) clone.setAttributeNS(xmlNs, 'xmlns', svgNs); - if (!clone.getAttribute('xmlns:xlink')) clone.setAttributeNS(xmlNs, 'xmlns:xlink', 'http://www.w3.org/1999/xlink'); + clone.setAttribute("version", "1.1"); + clone.setAttribute("viewBox", [left, top, width, height].join(" ")); + if (!clone.getAttribute("xmlns")) clone.setAttributeNS(xmlNs, "xmlns", svgNs); + if (!clone.getAttribute("xmlns:xlink")) + clone.setAttributeNS(xmlNs, "xmlns:xlink", "http://www.w3.org/1999/xlink"); if (responsive) { - clone.removeAttribute('width'); - clone.removeAttribute('height'); - clone.setAttribute('preserveAspectRatio', 'xMinYMin meet'); + clone.removeAttribute("width"); + clone.removeAttribute("height"); + clone.setAttribute("preserveAspectRatio", "xMinYMin meet"); } else { - clone.setAttribute('width', width * scale); - clone.setAttribute('height', height * scale); + clone.setAttribute("width", width * scale); + clone.setAttribute("height", height * scale); } - Array.from(clone.querySelectorAll('foreignObject > *')).forEach(foreignObject => { - foreignObject.setAttributeNS(xmlNs, 'xmlns', foreignObject.tagName === 'svg' ? svgNs : xhtmlNs); + Array.from(clone.querySelectorAll("foreignObject > *")).forEach((foreignObject) => { + foreignObject.setAttributeNS( + xmlNs, + "xmlns", + foreignObject.tagName === "svg" ? svgNs : xhtmlNs, + ); }); - return inlineCss(el, options).then(css => { - const style = document.createElement('style'); - style.setAttribute('type', 'text/css'); + return inlineCss(el, options).then((css) => { + const style = document.createElement("style"); + style.setAttribute("type", "text/css"); style.innerHTML = ``; - const defs = document.createElement('defs'); + const defs = document.createElement("defs"); defs.appendChild(style); clone.insertBefore(defs, clone.firstChild); - const outer = document.createElement('div'); + const outer = document.createElement("div"); outer.appendChild(clone); - const src = outer.innerHTML.replace(/NS\d+:href/gi, 'xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href'); + const src = outer.innerHTML.replace( + /NS\d+:href/gi, + 'xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href', + ); - if (typeof done === 'function') done(src, width, height); - else return {src, width, height}; + if (typeof done === "function") done(src, width, height); + else return { src, width, height }; }); }); }; out$.svgAsDataUri = (el, options, done) => { requireDomNode(el); - return out$.prepareSvg(el, options) - .then(({src, width, height}) => { - const svgXml = `data:image/svg+xml;base64,${window.btoa(reEncode(doctype+src))}`; - if (typeof done === 'function') { - done(svgXml, width, height); - } - return svgXml; - }); + return out$.prepareSvg(el, options).then(({ src, width, height }) => { + const svgXml = `data:image/svg+xml;base64,${window.btoa(reEncode(doctype + src))}`; + if (typeof done === "function") { + done(svgXml, width, height); + } + return svgXml; + }); }; out$.svgAsPngUri = (el, options, done) => { requireDomNode(el); - const { - encoderType = 'image/png', - encoderOptions = 0.8, - canvg - } = options || {}; + const { encoderType = "image/png", encoderOptions = 0.8, canvg } = options || {}; - const convertToPng = ({src, width, height}) => { - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); + const convertToPng = ({ src, width, height }) => { + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); canvas.width = width; canvas.height = height; @@ -347,39 +364,48 @@ THE SOFTWARE. try { png = canvas.toDataURL(encoderType, encoderOptions); } catch (e) { - if ((typeof SecurityError !== 'undefined' && e instanceof SecurityError) || e.name === 'SecurityError') { - console.error('Rendered SVG images cannot be downloaded in this browser.'); + if ( + (typeof SecurityError !== "undefined" && e instanceof SecurityError) || + e.name === "SecurityError" + ) { + console.error("Rendered SVG images cannot be downloaded in this browser."); return; } else throw e; } - if (typeof done === 'function') done(png, canvas.width, canvas.height); + if (typeof done === "function") done(png, canvas.width, canvas.height); return Promise.resolve(png); - } + }; if (canvg) return out$.prepareSvg(el, options).then(convertToPng); - else return out$.svgAsDataUri(el, options).then(uri => { - return new Promise((resolve, reject) => { - const image = new Image(); - image.onload = () => resolve(convertToPng({ - src: image, - width: image.width, - height: image.height - })); - image.onerror = () => { - reject(`There was an error loading the data URI as an image on the following SVG\n${window.atob(uri.slice(26))}Open the following link to see browser's diagnosis\n${uri}`); - } - image.src = uri; - }) - }); + else + return out$.svgAsDataUri(el, options).then((uri) => { + return new Promise((resolve, reject) => { + const image = new Image(); + image.onload = () => + resolve( + convertToPng({ + src: image, + width: image.width, + height: image.height, + }), + ); + image.onerror = () => { + reject( + `There was an error loading the data URI as an image on the following SVG\n${window.atob(uri.slice(26))}Open the following link to see browser's diagnosis\n${uri}`, + ); + }; + image.src = uri; + }); + }); }; out$.download = (name, uri, options) => { if (navigator.msSaveOrOpenBlob) navigator.msSaveOrOpenBlob(uriToBlob(uri), name); else { - const saveLink = document.createElement('a'); - if ('download' in saveLink) { + const saveLink = document.createElement("a"); + if ("download" in saveLink) { saveLink.download = name; - saveLink.style.display = 'none'; + saveLink.style.display = "none"; document.body.appendChild(saveLink); try { const blob = uriToBlob(uri); @@ -388,7 +414,7 @@ THE SOFTWARE. saveLink.onclick = () => requestAnimationFrame(() => URL.revokeObjectURL(url)); } catch (e) { console.error(e); - console.warn('Error while getting object URL. Falling back to string URL.'); + console.warn("Error while getting object URL. Falling back to string URL."); saveLink.href = uri; } saveLink.click(); @@ -403,14 +429,14 @@ THE SOFTWARE. out$.saveSvg = (el, name, options) => { const downloadOpts = downloadOptions(); // don't inline, can't be async return requireDomNodePromise(el) - .then(el => out$.svgAsDataUri(el, options || {})) - .then(uri => out$.download(name, uri, downloadOpts)); + .then((el) => out$.svgAsDataUri(el, options || {})) + .then((uri) => out$.download(name, uri, downloadOpts)); }; out$.saveSvgAsPng = (el, name, options) => { const downloadOpts = downloadOptions(); // don't inline, can't be async return requireDomNodePromise(el) - .then(el => out$.svgAsPngUri(el, options || {})) - .then(uri => out$.download(name, uri, downloadOpts)); + .then((el) => out$.svgAsPngUri(el, options || {})) + .then((uri) => out$.download(name, uri, downloadOpts)); }; })(); diff --git a/style-alt-theme.css b/style-alt-theme.css index 9701285..f2ac5d9 100644 --- a/style-alt-theme.css +++ b/style-alt-theme.css @@ -1,88 +1,87 @@ - /* Primary color theme */ :root { - --accent-color: hsl(210, 70%, 40%); - --accent-color-contrast: hsl(0, 0%, 55%); - - --background-color: hsl(0, 0%, 96%); - --background-color-contrast: hsl(0, 0%, 90%); - --background-color-contrast-more: hsl(0, 0%, 65%); - --background-color-graph: hsl(0, 0%, 100%); - - --background-color-inputs: hsl(0, 0%, 100%); - - --font-color-primary: hsl(0, 0%, 10%); - --font-color-secondary: hsl(0, 0%, 100%); - --font-color-inputs: hsl(0, 0%, 50%); - - --font-primary: 'Open Sans', sans-serif; - --font-secondary: monospace; - - --scrollbar-color: 255,255,255; - --scrollbar-accent: 0,0,0; - - --header-color: hsl(215, 20%, 10%); - --header-links-color: hsl(0, 0%, 100%); - --header-menu-icon-color: hsl(0, 0%, 100%); - - --logo-filter: invert(1.0); - --svg-filter: none; + --accent-color: hsl(210, 70%, 40%); + --accent-color-contrast: hsl(0, 0%, 55%); + + --background-color: hsl(0, 0%, 96%); + --background-color-contrast: hsl(0, 0%, 90%); + --background-color-contrast-more: hsl(0, 0%, 65%); + --background-color-graph: hsl(0, 0%, 100%); + + --background-color-inputs: hsl(0, 0%, 100%); + + --font-color-primary: hsl(0, 0%, 10%); + --font-color-secondary: hsl(0, 0%, 100%); + --font-color-inputs: hsl(0, 0%, 50%); + + --font-primary: "Open Sans", sans-serif; + --font-secondary: monospace; + + --scrollbar-color: 255, 255, 255; + --scrollbar-accent: 0, 0, 0; + + --header-color: hsl(215, 20%, 10%); + --header-links-color: hsl(0, 0%, 100%); + --header-menu-icon-color: hsl(0, 0%, 100%); + + --logo-filter: invert(1); + --svg-filter: none; } body.theme-dark { - --accent-color: hsl(0, 0%, 100%); - --accent-color-contrast: hsl(0, 0%, 50%); + --accent-color: hsl(0, 0%, 100%); + --accent-color-contrast: hsl(0, 0%, 50%); - --background-color: hsl(0, 0%, 6%); - --background-color-contrast: hsl(0, 0%, 15%); - --background-color-contrast-more: hsl(0, 0%, 30%); - --background-color-graph: hsl(0, 0%, 8%); + --background-color: hsl(0, 0%, 6%); + --background-color-contrast: hsl(0, 0%, 15%); + --background-color-contrast-more: hsl(0, 0%, 30%); + --background-color-graph: hsl(0, 0%, 8%); - --background-color-inputs: hsl(0, 0%, 15%); + --background-color-inputs: hsl(0, 0%, 15%); - --font-color-primary: hsl(0, 0%, 80%); - --font-color-secondary: hsl(0, 0%, 0%); - --font-color-inputs: hsl(0, 0%, 60%); + --font-color-primary: hsl(0, 0%, 80%); + --font-color-secondary: hsl(0, 0%, 0%); + --font-color-inputs: hsl(0, 0%, 60%); - --font-primary: 'Open Sans', sans-serif; - --font-secondary: monospace; + --font-primary: "Open Sans", sans-serif; + --font-secondary: monospace; - --scrollbar-color: 0,0,0; - --scrollbar-accent: 255,255,255; + --scrollbar-color: 0, 0, 0; + --scrollbar-accent: 255, 255, 255; - --header-color: hsl(0, 0%, 0%); - --header-links-color: hsl(0, 0%, 100%); - --header-menu-icon-color: hsl(0, 0%, 100%); - - --logo-filter: invert(1.0); - --svg-filter: invert(1); + --header-color: hsl(0, 0%, 0%); + --header-links-color: hsl(0, 0%, 100%); + --header-menu-icon-color: hsl(0, 0%, 100%); + + --logo-filter: invert(1); + --svg-filter: invert(1); } body.theme-contrast { - --accent-color: hsl(0, 0%, 0%); - --accent-color-contrast: hsl(0, 0%, 0%); - - --background-color: hsl(0, 0%, 100%); - --background-color-contrast: hsl(0, 0%, 0%); - --background-color-contrast-more: hsl(0, 0%, 0%); - --background-color-graph: hsl(0, 0%, 100%); - - --background-color-inputs: hsl(0, 0%, 100%); - - --font-color-primary: hsl(0, 0%, 0%); - --font-color-secondary: hsl(0, 0%, 100%); - --font-color-inputs: hsl(0, 0%, 0%); - - --font-primary: 'Open Sans', sans-serif; - --font-secondary: monospace; - - --scrollbar-color: 255,255,255; - --scrollbar-accent: 0,0,0; - - --header-color: hsl(215, 20%, 10%); - --header-links-color: hsl(0, 0%, 100%); - --header-menu-icon-color: hsl(0, 0%, 100%); - - --logo-filter: invert(1.0); - --svg-filter: none; + --accent-color: hsl(0, 0%, 0%); + --accent-color-contrast: hsl(0, 0%, 0%); + + --background-color: hsl(0, 0%, 100%); + --background-color-contrast: hsl(0, 0%, 0%); + --background-color-contrast-more: hsl(0, 0%, 0%); + --background-color-graph: hsl(0, 0%, 100%); + + --background-color-inputs: hsl(0, 0%, 100%); + + --font-color-primary: hsl(0, 0%, 0%); + --font-color-secondary: hsl(0, 0%, 100%); + --font-color-inputs: hsl(0, 0%, 0%); + + --font-primary: "Open Sans", sans-serif; + --font-secondary: monospace; + + --scrollbar-color: 255, 255, 255; + --scrollbar-accent: 0, 0, 0; + + --header-color: hsl(215, 20%, 10%); + --header-links-color: hsl(0, 0%, 100%); + --header-menu-icon-color: hsl(0, 0%, 100%); + + --logo-filter: invert(1); + --svg-filter: none; } diff --git a/style-alt.css b/style-alt.css index bfd4d0d..4979824 100644 --- a/style-alt.css +++ b/style-alt.css @@ -1,456 +1,555 @@ - /***** Browser scrollbar styles *****/ ::-webkit-scrollbar { - width: 6px; - height: 6px; - - background-color: rgba(var(--scrollbar-color), 0.1); - border-radius: 10px; + width: 6px; + height: 6px; + + background-color: rgba(var(--scrollbar-color), 0.1); + border-radius: 10px; } ::-webkit-scrollbar:hover { - background-color: rgba(var(--scrollbar-color), 0.1); + background-color: rgba(var(--scrollbar-color), 0.1); } ::-webkit-scrollbar-thumb { - background-color: rgba(var(--scrollbar-accent), 0.1); - border-radius: 10px; + background-color: rgba(var(--scrollbar-accent), 0.1); + border-radius: 10px; } ::-webkit-scrollbar-thumb:vertical:active, ::-webkit-scrollbar-thumb:horizontal:active { - background-color: rgba(var(--scrollbar-accent), 0.2); + background-color: rgba(var(--scrollbar-accent), 0.2); } /* For Firefox, maybe? */ html { - scrollbar-color: dark; + scrollbar-color: dark; } - /***** Icons *****/ /***** https://yoksel.github.io/url-encoder/ *****/ :root { - --icon-remove: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23000000;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M95.36,24.64h0a5,5,0,0,0-7.08,0L60,52.93,31.72,24.64a5,5,0,0,0-7.08,0h0a5,5,0,0,0,0,7.08L52.93,60,24.64,88.28a5,5,0,0,0,0,7.08h0a5,5,0,0,0,7.08,0L60,67.07,88.28,95.36a5,5,0,0,0,7.08,0h0a5,5,0,0,0,0-7.08L67.07,60,95.36,31.72A5,5,0,0,0,95.36,24.64Z'/%3E%3C/svg%3E"); - --icon-baseline: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Crect x='10' y='55' width='100' height='10' rx='5'/%3E%3C/svg%3E"); - --icon-squiggle: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23000000;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M80.37,85c-12.63,0-18.92-6.89-22-12.67A30.8,30.8,0,0,1,55,60.18V60a20.12,20.12,0,0,0-2.1-8c-2.46-4.7-6.68-7-12.9-7s-10.44,2.28-12.9,7A20.1,20.1,0,0,0,25,60a5,5,0,0,1-5,5h0a5,5,0,0,1-5-5c0-8.65,5.22-25,25-25S65,51.2,65,59.87C65.1,61.67,66.3,75,80.37,75c6,0,10.16-2.27,12.56-7A20.49,20.49,0,0,0,95,60a5,5,0,0,1,5-5h0a5,5,0,0,1,5,5,30,30,0,0,1-3,12.2C98,80.46,90.29,85,80.37,85Z'/%3E%3C/svg%3E"); - --icon-download: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 14L11.2929 14.7071L12 15.4142L12.7071 14.7071L12 14ZM13 5C13 4.44772 12.5523 4 12 4C11.4477 4 11 4.44771 11 5L13 5ZM6.29289 9.70711L11.2929 14.7071L12.7071 13.2929L7.70711 8.29289L6.29289 9.70711ZM12.7071 14.7071L17.7071 9.70711L16.2929 8.29289L11.2929 13.2929L12.7071 14.7071ZM13 14L13 5L11 5L11 14L13 14Z' fill='%23CCD2E3'/%3E%3Cpath d='M5 16L5 17C5 18.1046 5.89543 19 7 19L17 19C18.1046 19 19 18.1046 19 17V16' stroke='%23CCD2E3' stroke-width='2'/%3E%3C/svg%3E%0A"); - --icon-hide: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23000000;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M60,30C32.39,30,10,43.43,10,60S32.39,90,60,90s50-13.43,50-30S87.61,30,60,30ZM90.21,72.64C82.41,77.32,71.4,80,60,80,37.11,80,20,69.44,20,60c0-4.3,3.57-8.91,9.79-12.64C37.59,42.68,48.6,40,60,40c22.89,0,40,10.56,40,20C100,64.3,96.43,68.91,90.21,72.64Z'/%3E%3Ccircle class='cls-1' cx='60' cy='60' r='10'/%3E%3C/svg%3E"); - --icon-pin: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23000000;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M78.07,30.48A28.68,28.68,0,0,1,89.52,41.93l-2.33-.64L83.49,45,69.05,59.43l-4.78,4.78,3.27,5.93c2.61,4.74,2.9,7.76,2.79,8.93a3,3,0,0,1-.45,0c-2.64,0-7.89-1.7-14-6.48l-4.75-3.74-3.74-4.75c-5.55-7.09-6.65-12.6-6.48-14.45l.46,0c1,0,3.89.28,8.49,2.81l5.93,3.27L60.57,51,75,36.51l3.71-3.7-.64-2.33M74.41,20.27A6.34,6.34,0,0,0,69.77,22c-1.79,1.79-2.08,4.76-1.13,8.2L54.21,44.58C49.57,42,45.1,40.65,41.37,40.65a9.54,9.54,0,0,0-7,2.51c-5,5-2.27,16.09,5.9,26.51L23,95.58a1,1,0,0,0,.83,1.55,1,1,0,0,0,.55-.17L50.33,79.69c6.87,5.38,14,8.4,19.55,8.4a9.52,9.52,0,0,0,7-2.5c3.93-3.93,3.08-11.62-1.42-19.8L89.85,51.36a13.62,13.62,0,0,0,3.58.53,6.32,6.32,0,0,0,4.62-1.66C102,46.32,98.79,36.83,91,29c-5.54-5.54-11.93-8.75-16.57-8.75Z'/%3E%3C/svg%3E"); - --icon-plus: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23000000;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M110,60h0a5,5,0,0,0-5-5H65V15a5,5,0,0,0-5-5h0a5,5,0,0,0-5,5V55H15a5,5,0,0,0-5,5h0a5,5,0,0,0,5,5H55v40a5,5,0,0,0,5,5h0a5,5,0,0,0,5-5V65h40A5,5,0,0,0,110,60Z'/%3E%3C/svg%3E"); - --icon-new-tab: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cdefs%3E%3Cstyle%3E.cls-1,.cls-2%7Bfill:none;stroke:%23231f20;%7D.cls-2%7Bstroke-linecap:round;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M21,11v2c0,3.77,0,5.66-1.17,6.83S16.77,21,13,21H11c-3.77,0-5.66,0-6.83-1.17S3,16.77,3,13V11C3,7.23,3,5.34,4.17,4.17S7.23,3,11,3h1'/%3E%3Cpath class='cls-2' d='M21,3.15H16.76m4.24,0V7.39m0-4.24-8.49,8.48'/%3E%3C/svg%3E"); - - --icon-expand: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23252627;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M24.64,95.36l24.75-3.54-7.07-7.07,3.54-3.54a5,5,0,0,0,0-7.07h0a5,5,0,0,0-7.07,0l-3.54,3.54-7.07-7.07Z'/%3E%3Cpath class='cls-1' d='M95.36,24.64,70.61,28.18l7.07,7.07-3.54,3.54a5,5,0,0,0,0,7.07h0a5,5,0,0,0,7.07,0l3.54-3.54,7.07,7.07Z'/%3E%3Cpath class='cls-1' d='M42.32,35.25l7.07-7.07L24.64,24.64l3.54,24.75,7.07-7.07,3.54,3.54a5,5,0,0,0,7.07,0h0a5,5,0,0,0,0-7.07Z'/%3E%3Cpath class='cls-1' d='M95.36,95.36,91.82,70.61l-7.07,7.07-3.54-3.54a5,5,0,0,0-7.07,0h0a5,5,0,0,0,0,7.07l3.54,3.54-7.07,7.07Z'/%3E%3C/svg%3E"); - --icon-collapse: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23252627;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M52.93,67.07,28.18,70.61l7.07,7.07-3.53,3.53a5,5,0,0,0,0,7.07h0a5,5,0,0,0,7.07,0l3.53-3.53,7.07,7.07Z'/%3E%3Cpath class='cls-1' d='M67.07,52.93l24.75-3.54-7.07-7.07,3.53-3.53a5,5,0,0,0,0-7.07h0a5,5,0,0,0-7.07,0l-3.53,3.53-7.07-7.07Z'/%3E%3C/svg%3E"); - - --icon-expand: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23252627;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M45,105h0a5,5,0,0,1-5,5H10V80a5,5,0,0,1,5-5h0a5,5,0,0,1,5,5v20H40A5,5,0,0,1,45,105Z'/%3E%3Cpath class='cls-1' d='M110,80v30H80a5,5,0,0,1-5-5h0a5,5,0,0,1,5-5h20V80a5,5,0,0,1,5-5h0A5,5,0,0,1,110,80Z'/%3E%3Cpath class='cls-1' d='M110,10V40a5,5,0,0,1-5,5h0a5,5,0,0,1-5-5V20H80a5,5,0,0,1-5-5h0a5,5,0,0,1,5-5Z'/%3E%3Cpath class='cls-1' d='M45,15h0a5,5,0,0,1-5,5H20V40a5,5,0,0,1-5,5h0a5,5,0,0,1-5-5V10H40A5,5,0,0,1,45,15Z'/%3E%3C/svg%3E"); - --icon-collapse: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23252627;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M10,80h0a5,5,0,0,1,5-5H45v30a5,5,0,0,1-5,5h0a5,5,0,0,1-5-5V85H15A5,5,0,0,1,10,80Z'/%3E%3Cpath class='cls-1' d='M75,105V75h30a5,5,0,0,1,5,5h0a5,5,0,0,1-5,5H85v20a5,5,0,0,1-5,5h0A5,5,0,0,1,75,105Z'/%3E%3Cpath class='cls-1' d='M75,45V15a5,5,0,0,1,5-5h0a5,5,0,0,1,5,5V35h20a5,5,0,0,1,5,5h0a5,5,0,0,1-5,5Z'/%3E%3Cpath class='cls-1' d='M10,40h0a5,5,0,0,1,5-5H35V15a5,5,0,0,1,5-5h0a5,5,0,0,1,5,5V45H15A5,5,0,0,1,10,40Z'/%3E%3C/svg%3E"); - - --icon-hamburger: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 120 120' style='enable-background:new 0 0 120 120;' xml:space='preserve'%3E%3Cpath class='st0' d='M105,65H15c-2.76,0-5-2.24-5-5v0c0-2.76,2.24-5,5-5h90c2.76,0,5,2.24,5,5v0C110,62.76,107.76,65,105,65z'/%3E%3Cpath class='st0' d='M105,20H15c-2.76,0-5-2.24-5-5v0c0-2.76,2.24-5,5-5h90c2.76,0,5,2.24,5,5v0C110,17.76,107.76,20,105,20z'/%3E%3Cpath class='st0' d='M105,110H15c-2.76,0-5-2.24-5-5v0c0-2.76,2.24-5,5-5h90c2.76,0,5,2.24,5,5v0C110,107.76,107.76,110,105,110z'/%3E%3C/svg%3E%0A"); + --icon-remove: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23000000;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M95.36,24.64h0a5,5,0,0,0-7.08,0L60,52.93,31.72,24.64a5,5,0,0,0-7.08,0h0a5,5,0,0,0,0,7.08L52.93,60,24.64,88.28a5,5,0,0,0,0,7.08h0a5,5,0,0,0,7.08,0L60,67.07,88.28,95.36a5,5,0,0,0,7.08,0h0a5,5,0,0,0,0-7.08L67.07,60,95.36,31.72A5,5,0,0,0,95.36,24.64Z'/%3E%3C/svg%3E"); + --icon-baseline: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Crect x='10' y='55' width='100' height='10' rx='5'/%3E%3C/svg%3E"); + --icon-squiggle: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23000000;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M80.37,85c-12.63,0-18.92-6.89-22-12.67A30.8,30.8,0,0,1,55,60.18V60a20.12,20.12,0,0,0-2.1-8c-2.46-4.7-6.68-7-12.9-7s-10.44,2.28-12.9,7A20.1,20.1,0,0,0,25,60a5,5,0,0,1-5,5h0a5,5,0,0,1-5-5c0-8.65,5.22-25,25-25S65,51.2,65,59.87C65.1,61.67,66.3,75,80.37,75c6,0,10.16-2.27,12.56-7A20.49,20.49,0,0,0,95,60a5,5,0,0,1,5-5h0a5,5,0,0,1,5,5,30,30,0,0,1-3,12.2C98,80.46,90.29,85,80.37,85Z'/%3E%3C/svg%3E"); + --icon-download: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 14L11.2929 14.7071L12 15.4142L12.7071 14.7071L12 14ZM13 5C13 4.44772 12.5523 4 12 4C11.4477 4 11 4.44771 11 5L13 5ZM6.29289 9.70711L11.2929 14.7071L12.7071 13.2929L7.70711 8.29289L6.29289 9.70711ZM12.7071 14.7071L17.7071 9.70711L16.2929 8.29289L11.2929 13.2929L12.7071 14.7071ZM13 14L13 5L11 5L11 14L13 14Z' fill='%23CCD2E3'/%3E%3Cpath d='M5 16L5 17C5 18.1046 5.89543 19 7 19L17 19C18.1046 19 19 18.1046 19 17V16' stroke='%23CCD2E3' stroke-width='2'/%3E%3C/svg%3E%0A"); + --icon-hide: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23000000;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M60,30C32.39,30,10,43.43,10,60S32.39,90,60,90s50-13.43,50-30S87.61,30,60,30ZM90.21,72.64C82.41,77.32,71.4,80,60,80,37.11,80,20,69.44,20,60c0-4.3,3.57-8.91,9.79-12.64C37.59,42.68,48.6,40,60,40c22.89,0,40,10.56,40,20C100,64.3,96.43,68.91,90.21,72.64Z'/%3E%3Ccircle class='cls-1' cx='60' cy='60' r='10'/%3E%3C/svg%3E"); + --icon-pin: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23000000;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M78.07,30.48A28.68,28.68,0,0,1,89.52,41.93l-2.33-.64L83.49,45,69.05,59.43l-4.78,4.78,3.27,5.93c2.61,4.74,2.9,7.76,2.79,8.93a3,3,0,0,1-.45,0c-2.64,0-7.89-1.7-14-6.48l-4.75-3.74-3.74-4.75c-5.55-7.09-6.65-12.6-6.48-14.45l.46,0c1,0,3.89.28,8.49,2.81l5.93,3.27L60.57,51,75,36.51l3.71-3.7-.64-2.33M74.41,20.27A6.34,6.34,0,0,0,69.77,22c-1.79,1.79-2.08,4.76-1.13,8.2L54.21,44.58C49.57,42,45.1,40.65,41.37,40.65a9.54,9.54,0,0,0-7,2.51c-5,5-2.27,16.09,5.9,26.51L23,95.58a1,1,0,0,0,.83,1.55,1,1,0,0,0,.55-.17L50.33,79.69c6.87,5.38,14,8.4,19.55,8.4a9.52,9.52,0,0,0,7-2.5c3.93-3.93,3.08-11.62-1.42-19.8L89.85,51.36a13.62,13.62,0,0,0,3.58.53,6.32,6.32,0,0,0,4.62-1.66C102,46.32,98.79,36.83,91,29c-5.54-5.54-11.93-8.75-16.57-8.75Z'/%3E%3C/svg%3E"); + --icon-plus: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23000000;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M110,60h0a5,5,0,0,0-5-5H65V15a5,5,0,0,0-5-5h0a5,5,0,0,0-5,5V55H15a5,5,0,0,0-5,5h0a5,5,0,0,0,5,5H55v40a5,5,0,0,0,5,5h0a5,5,0,0,0,5-5V65h40A5,5,0,0,0,110,60Z'/%3E%3C/svg%3E"); + --icon-new-tab: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cdefs%3E%3Cstyle%3E.cls-1,.cls-2%7Bfill:none;stroke:%23231f20;%7D.cls-2%7Bstroke-linecap:round;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M21,11v2c0,3.77,0,5.66-1.17,6.83S16.77,21,13,21H11c-3.77,0-5.66,0-6.83-1.17S3,16.77,3,13V11C3,7.23,3,5.34,4.17,4.17S7.23,3,11,3h1'/%3E%3Cpath class='cls-2' d='M21,3.15H16.76m4.24,0V7.39m0-4.24-8.49,8.48'/%3E%3C/svg%3E"); + + --icon-expand: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23252627;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M24.64,95.36l24.75-3.54-7.07-7.07,3.54-3.54a5,5,0,0,0,0-7.07h0a5,5,0,0,0-7.07,0l-3.54,3.54-7.07-7.07Z'/%3E%3Cpath class='cls-1' d='M95.36,24.64,70.61,28.18l7.07,7.07-3.54,3.54a5,5,0,0,0,0,7.07h0a5,5,0,0,0,7.07,0l3.54-3.54,7.07,7.07Z'/%3E%3Cpath class='cls-1' d='M42.32,35.25l7.07-7.07L24.64,24.64l3.54,24.75,7.07-7.07,3.54,3.54a5,5,0,0,0,7.07,0h0a5,5,0,0,0,0-7.07Z'/%3E%3Cpath class='cls-1' d='M95.36,95.36,91.82,70.61l-7.07,7.07-3.54-3.54a5,5,0,0,0-7.07,0h0a5,5,0,0,0,0,7.07l3.54,3.54-7.07,7.07Z'/%3E%3C/svg%3E"); + --icon-collapse: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23252627;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M52.93,67.07,28.18,70.61l7.07,7.07-3.53,3.53a5,5,0,0,0,0,7.07h0a5,5,0,0,0,7.07,0l3.53-3.53,7.07,7.07Z'/%3E%3Cpath class='cls-1' d='M67.07,52.93l24.75-3.54-7.07-7.07,3.53-3.53a5,5,0,0,0,0-7.07h0a5,5,0,0,0-7.07,0l-3.53,3.53-7.07-7.07Z'/%3E%3C/svg%3E"); + + --icon-expand: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23252627;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M45,105h0a5,5,0,0,1-5,5H10V80a5,5,0,0,1,5-5h0a5,5,0,0,1,5,5v20H40A5,5,0,0,1,45,105Z'/%3E%3Cpath class='cls-1' d='M110,80v30H80a5,5,0,0,1-5-5h0a5,5,0,0,1,5-5h20V80a5,5,0,0,1,5-5h0A5,5,0,0,1,110,80Z'/%3E%3Cpath class='cls-1' d='M110,10V40a5,5,0,0,1-5,5h0a5,5,0,0,1-5-5V20H80a5,5,0,0,1-5-5h0a5,5,0,0,1,5-5Z'/%3E%3Cpath class='cls-1' d='M45,15h0a5,5,0,0,1-5,5H20V40a5,5,0,0,1-5,5h0a5,5,0,0,1-5-5V10H40A5,5,0,0,1,45,15Z'/%3E%3C/svg%3E"); + --icon-collapse: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23252627;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M10,80h0a5,5,0,0,1,5-5H45v30a5,5,0,0,1-5,5h0a5,5,0,0,1-5-5V85H15A5,5,0,0,1,10,80Z'/%3E%3Cpath class='cls-1' d='M75,105V75h30a5,5,0,0,1,5,5h0a5,5,0,0,1-5,5H85v20a5,5,0,0,1-5,5h0A5,5,0,0,1,75,105Z'/%3E%3Cpath class='cls-1' d='M75,45V15a5,5,0,0,1,5-5h0a5,5,0,0,1,5,5V35h20a5,5,0,0,1,5,5h0a5,5,0,0,1-5,5Z'/%3E%3Cpath class='cls-1' d='M10,40h0a5,5,0,0,1,5-5H35V15a5,5,0,0,1,5-5h0a5,5,0,0,1,5,5V45H15A5,5,0,0,1,10,40Z'/%3E%3C/svg%3E"); + + --icon-hamburger: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 120 120' style='enable-background:new 0 0 120 120;' xml:space='preserve'%3E%3Cpath class='st0' d='M105,65H15c-2.76,0-5-2.24-5-5v0c0-2.76,2.24-5,5-5h90c2.76,0,5,2.24,5,5v0C110,62.76,107.76,65,105,65z'/%3E%3Cpath class='st0' d='M105,20H15c-2.76,0-5-2.24-5-5v0c0-2.76,2.24-5,5-5h90c2.76,0,5,2.24,5,5v0C110,17.76,107.76,20,105,20z'/%3E%3Cpath class='st0' d='M105,110H15c-2.76,0-5-2.24-5-5v0c0-2.76,2.24-5,5-5h90c2.76,0,5,2.24,5,5v0C110,107.76,107.76,110,105,110z'/%3E%3C/svg%3E%0A"); } /***** Base page styles *****/ html { - width: 100vw; - height: 100vh; - max-height: -webkit-fill-available; - overflow: hidden; - - font-family: var(--font-primary); - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -webkit-text-size-adjust: none; - - user-select: none; - scroll-behavior: smooth; - overscroll-behavior-y: none; + width: 100vw; + height: 100vh; + max-height: -webkit-fill-available; + overflow: hidden; + + font-family: var(--font-primary); + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -webkit-text-size-adjust: none; + + user-select: none; + scroll-behavior: smooth; + overscroll-behavior-y: none; } body { - margin: 0; - width: 100vw; - height: 100vh; - max-height: -webkit-fill-available; - overflow: hidden; - - background-color: var(--background-color); - - color: var(--font-color-primary); - font-family: var(--font-primary); - font-weight: 400; - font-size: 14px; - line-height: 1em; - - opacity: 1.0; + margin: 0; + width: 100vw; + height: 100vh; + max-height: -webkit-fill-available; + overflow: hidden; + + background-color: var(--background-color); + + color: var(--font-color-primary); + font-family: var(--font-primary); + font-weight: 400; + font-size: 14px; + line-height: 1em; + + opacity: 1; } input { - -webkit-appearance: none; + -webkit-appearance: none; } - - /***** Graph colors *****/ svg text { - color: var(--background-color-contrast-more); - color: var(--font-color-primary); - font-family: var(--font-secondary); + color: var(--background-color-contrast-more); + color: var(--font-color-primary); + font-family: var(--font-secondary); } svg text.graph-name { - fill: var(--font-color-primary); - - font-family: var(--font-primary); - font-weight: 700; + fill: var(--font-color-primary); + + font-family: var(--font-primary); + font-weight: 700; } svg line { - stroke: var(--background-color-contrast-more); + stroke: var(--background-color-contrast-more); } g.lineLabel rect { - fill: var(--background-color-graph); - filter: blur(2px); - opacity: 0.9; + fill: var(--background-color-graph); + filter: blur(2px); + opacity: 0.9; } g.lineLabel text { - font-family: var(--font-primary); - font-weight: 700; + font-family: var(--font-primary); + font-weight: 700; } svg#fr-graph { - position: relative; - - background-color: var(--background-color-graph); - - pointer-events: none; + position: relative; + + background-color: var(--background-color-graph); + + pointer-events: none; } rect.graphBackground { - opacity: 0.0; + opacity: 0; } g.dBScaler { - fill: var(--background-color-contrast-more); - pointer-events: all; + fill: var(--background-color-contrast-more); + pointer-events: all; } g.dBScaler rect[fill="#bbb"] { - fill: var(--background-color-graph); + fill: var(--background-color-graph); } svg#fr-graph > g > image { - filter: var(--svg-filter); + filter: var(--svg-filter); } svg#fr-graph rect { - pointer-events: all; + pointer-events: all; } g.inspector rect { - pointer-events: none !important; + pointer-events: none !important; } text.insp_dB { - fill: var(--background-color-contrast-more); + fill: var(--background-color-contrast-more); } /* Animating graph stroke */ div.graphBox[data-animated="true"] g[mask="url(#graphFade)"] path:not(.target) { - stroke-dasharray: 2000px; - - animation-name: graph-draw; - animation-duration: 1.0s; - animation-iteration-count: 1; - animation-timing-function: ease-out; - animation-fill-mode: forwards; + stroke-dasharray: 2000px; + + animation-name: graph-draw; + animation-duration: 1s; + animation-iteration-count: 1; + animation-timing-function: ease-out; + animation-fill-mode: forwards; } @keyframes graph-draw { - 0% { - stroke-dashoffset: 2000px; - } - - 100% { - stroke-dashoffset: 0px; - } + 0% { + stroke-dashoffset: 2000px; + } + + 100% { + stroke-dashoffset: 0px; + } } /***** Graph labels *****/ /* top-left */ -svg#fr-graph[data-labels-position="top-left"] > rect + g.lineLabel + g.lineLabel + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(1), -svg#fr-graph[data-labels-position="top-left"] > rect + g.lineLabel + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(1), -svg#fr-graph[data-labels-position="top-left"] > rect + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(1), +svg#fr-graph[data-labels-position="top-left"] + > rect + + g.lineLabel + + g.lineLabel + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(1), +svg#fr-graph[data-labels-position="top-left"] + > rect + + g.lineLabel + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(1), +svg#fr-graph[data-labels-position="top-left"] + > rect + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(1), svg#fr-graph[data-labels-position="top-left"] > rect + g.lineLabel + g.lineLabel:nth-last-child(1), svg#fr-graph[data-labels-position="top-left"] > rect + g.lineLabel:nth-last-child(1) { - transform: translate(40px, 8px); -} - -svg#fr-graph[data-labels-position="top-left"] > rect + g.lineLabel + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(2), -svg#fr-graph[data-labels-position="top-left"] > rect + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(2), + transform: translate(40px, 8px); +} + +svg#fr-graph[data-labels-position="top-left"] + > rect + + g.lineLabel + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(2), +svg#fr-graph[data-labels-position="top-left"] + > rect + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(2), svg#fr-graph[data-labels-position="top-left"] > rect + g.lineLabel + g.lineLabel:nth-last-child(2), svg#fr-graph[data-labels-position="top-left"] rect + g.lineLabel:nth-last-child(2) { - transform: translate(40px, 30px); + transform: translate(40px, 30px); } -svg#fr-graph[data-labels-position="top-left"] rect + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(3), +svg#fr-graph[data-labels-position="top-left"] + rect + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(3), svg#fr-graph[data-labels-position="top-left"] rect + g.lineLabel + g.lineLabel:nth-last-child(3), svg#fr-graph[data-labels-position="top-left"] rect + g.lineLabel:nth-last-child(3) { - transform: translate(40px, 52px); + transform: translate(40px, 52px); } svg#fr-graph[data-labels-position="top-left"] > rect + g.lineLabel + g.lineLabel:nth-last-child(4), svg#fr-graph[data-labels-position="top-left"] > rect + g.lineLabel:nth-last-child(4) { - transform: translate(40px, 74px); + transform: translate(40px, 74px); } svg#fr-graph[data-labels-position="top-left"] > rect + g.lineLabel:nth-last-child(5) { - transform: translate(40px, 96px); + transform: translate(40px, 96px); } /* bottom-left */ -svg#fr-graph[data-labels-position="bottom-left"] > rect + g.lineLabel + g.lineLabel + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(1), -svg#fr-graph[data-labels-position="bottom-left"] > rect + g.lineLabel + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(1), -svg#fr-graph[data-labels-position="bottom-left"] > rect + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(1), -svg#fr-graph[data-labels-position="bottom-left"] > rect + g.lineLabel + g.lineLabel:nth-last-child(1), +svg#fr-graph[data-labels-position="bottom-left"] + > rect + + g.lineLabel + + g.lineLabel + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(1), +svg#fr-graph[data-labels-position="bottom-left"] + > rect + + g.lineLabel + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(1), +svg#fr-graph[data-labels-position="bottom-left"] + > rect + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(1), +svg#fr-graph[data-labels-position="bottom-left"] + > rect + + g.lineLabel + + g.lineLabel:nth-last-child(1), svg#fr-graph[data-labels-position="bottom-left"] > rect + g.lineLabel:nth-last-child(1) { - transform: translate(40px, 296px); -} - -svg#fr-graph[data-labels-position="bottom-left"] > rect + g.lineLabel + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(2), -svg#fr-graph[data-labels-position="bottom-left"] > rect + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(2), -svg#fr-graph[data-labels-position="bottom-left"] > rect + g.lineLabel + g.lineLabel:nth-last-child(2), + transform: translate(40px, 296px); +} + +svg#fr-graph[data-labels-position="bottom-left"] + > rect + + g.lineLabel + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(2), +svg#fr-graph[data-labels-position="bottom-left"] + > rect + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(2), +svg#fr-graph[data-labels-position="bottom-left"] + > rect + + g.lineLabel + + g.lineLabel:nth-last-child(2), svg#fr-graph[data-labels-position="bottom-left"] rect + g.lineLabel:nth-last-child(2) { - transform: translate(40px, 274px); + transform: translate(40px, 274px); } -svg#fr-graph[data-labels-position="bottom-left"] rect + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(3), +svg#fr-graph[data-labels-position="bottom-left"] + rect + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(3), svg#fr-graph[data-labels-position="bottom-left"] rect + g.lineLabel + g.lineLabel:nth-last-child(3), svg#fr-graph[data-labels-position="bottom-left"] rect + g.lineLabel:nth-last-child(3) { - transform: translate(40px, 252px); + transform: translate(40px, 252px); } -svg#fr-graph[data-labels-position="bottom-left"] > rect + g.lineLabel + g.lineLabel:nth-last-child(4), +svg#fr-graph[data-labels-position="bottom-left"] + > rect + + g.lineLabel + + g.lineLabel:nth-last-child(4), svg#fr-graph[data-labels-position="bottom-left"] > rect + g.lineLabel:nth-last-child(4) { - transform: translate(40px, 230px); + transform: translate(40px, 230px); } svg#fr-graph[data-labels-position="bottom-left"] > rect + g.lineLabel:nth-last-child(5) { - transform: translate(40px, 208px); + transform: translate(40px, 208px); } /* bottom-right */ -svg#fr-graph[data-labels-position="bottom-right"] > rect + g.lineLabel + g.lineLabel + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(1), -svg#fr-graph[data-labels-position="bottom-right"] > rect + g.lineLabel + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(1), -svg#fr-graph[data-labels-position="bottom-right"] > rect + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(1), -svg#fr-graph[data-labels-position="bottom-right"] > rect + g.lineLabel + g.lineLabel:nth-last-child(1), +svg#fr-graph[data-labels-position="bottom-right"] + > rect + + g.lineLabel + + g.lineLabel + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(1), +svg#fr-graph[data-labels-position="bottom-right"] + > rect + + g.lineLabel + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(1), +svg#fr-graph[data-labels-position="bottom-right"] + > rect + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(1), +svg#fr-graph[data-labels-position="bottom-right"] + > rect + + g.lineLabel + + g.lineLabel:nth-last-child(1), svg#fr-graph[data-labels-position="bottom-right"] > rect + g.lineLabel:nth-last-child(1) { - transform: translate(500px, 296px); -} - -svg#fr-graph[data-labels-position="bottom-right"] > rect + g.lineLabel + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(2), -svg#fr-graph[data-labels-position="bottom-right"] > rect + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(2), -svg#fr-graph[data-labels-position="bottom-right"] > rect + g.lineLabel + g.lineLabel:nth-last-child(2), + transform: translate(500px, 296px); +} + +svg#fr-graph[data-labels-position="bottom-right"] + > rect + + g.lineLabel + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(2), +svg#fr-graph[data-labels-position="bottom-right"] + > rect + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(2), +svg#fr-graph[data-labels-position="bottom-right"] + > rect + + g.lineLabel + + g.lineLabel:nth-last-child(2), svg#fr-graph[data-labels-position="bottom-right"] rect + g.lineLabel:nth-last-child(2) { - transform: translate(500px, 274px); -} - -svg#fr-graph[data-labels-position="bottom-right"] rect + g.lineLabel + g.lineLabel + g.lineLabel:nth-last-child(3), -svg#fr-graph[data-labels-position="bottom-right"] rect + g.lineLabel + g.lineLabel:nth-last-child(3), + transform: translate(500px, 274px); +} + +svg#fr-graph[data-labels-position="bottom-right"] + rect + + g.lineLabel + + g.lineLabel + + g.lineLabel:nth-last-child(3), +svg#fr-graph[data-labels-position="bottom-right"] + rect + + g.lineLabel + + g.lineLabel:nth-last-child(3), svg#fr-graph[data-labels-position="bottom-right"] rect + g.lineLabel:nth-last-child(3) { - transform: translate(500px, 252px); + transform: translate(500px, 252px); } -svg#fr-graph[data-labels-position="bottom-right"] > rect + g.lineLabel + g.lineLabel:nth-last-child(4), +svg#fr-graph[data-labels-position="bottom-right"] + > rect + + g.lineLabel + + g.lineLabel:nth-last-child(4), svg#fr-graph[data-labels-position="bottom-right"] > rect + g.lineLabel:nth-last-child(4) { - transform: translate(500px, 230px); + transform: translate(500px, 230px); } svg#fr-graph[data-labels-position="bottom-right"] > rect + g.lineLabel:nth-last-child(5) { - transform: translate(500px, 208px); + transform: translate(500px, 208px); } /***** Framing *****/ div.graphtool { - display: flex; - flex-direction: column; - box-sizing: border-box; - height: 100vh; - max-height: -webkit-fill-available; + display: flex; + flex-direction: column; + box-sizing: border-box; + height: 100vh; + max-height: -webkit-fill-available; } main.main { - display: flex; - flex: 100% 1 1; - overflow: hidden; + display: flex; + flex: 100% 1 1; + overflow: hidden; } section.parts-primary { - position: relative; - z-index: 1; - - flex: auto 1 1; - order: 2; - - box-sizing: border-box; - height: 100%; - - background-color: var(--background-color); - - overflow-y: auto; + position: relative; + z-index: 1; + + flex: auto 1 1; + order: 2; + + box-sizing: border-box; + height: 100%; + + background-color: var(--background-color); + + overflow-y: auto; } section.parts-secondary { - flex: 350px 0 0; - order: 1; - - box-sizing: border-box; - height: 100%; - - border-right: 1px solid var(--background-color); + flex: 350px 0 0; + order: 1; + + box-sizing: border-box; + height: 100%; + + border-right: 1px solid var(--background-color); } /***** Alt header *****/ header.header { - display: flex; - align-items: flex-start; - - box-sizing: border-box; - - background-color: var(--header-color); + display: flex; + align-items: flex-start; + + box-sizing: border-box; + + background-color: var(--header-color); } button.header-button { - display: none; + display: none; } div.logo { - display: flex; - justify-content: center; - align-items: center; - - box-sizing: border-box; - flex: 350px 0 0; - height: 48px; - padding: 12px 0; - overflow: hidden; - - background-size: contain; - background-repeat: no-repeat; - background-position: center left; + display: flex; + justify-content: center; + align-items: center; + + box-sizing: border-box; + flex: 350px 0 0; + height: 48px; + padding: 12px 0; + overflow: hidden; + + background-size: contain; + background-repeat: no-repeat; + background-position: center left; } div.logo a { - display: block; - width: 100%; - height: 100%; - text-decoration: none; + display: block; + width: 100%; + height: 100%; + text-decoration: none; } div.logo img { - width: 100%; - height: 100%; - object-fit: contain; - - filter: var(--logo-filter); + width: 100%; + height: 100%; + object-fit: contain; + + filter: var(--logo-filter); } div.logo span { - margin: 0 1em; - font-size: 18px; - font-weight: bold; - line-height: 24px; - - color: var(--header-links-color); + margin: 0 1em; + font-size: 18px; + font-weight: bold; + line-height: 24px; + + color: var(--header-links-color); } ul.header-links { - display: flex; - justify-content: flex-start; - align-items: flex-start; - flex-wrap: wrap; - gap: 0 32px; - - box-sizing: border-box; - flex: 100% 0 1; - height: 100%; - margin: auto; - padding: 0 16px; - - overflow-x: hidden; + display: flex; + justify-content: flex-start; + align-items: flex-start; + flex-wrap: wrap; + gap: 0 32px; + + box-sizing: border-box; + flex: 100% 0 1; + height: 100%; + margin: auto; + padding: 0 16px; + + overflow-x: hidden; } ul.header-links::-webkit-scrollbar { - width: 0; - height: 0; + width: 0; + height: 0; } ul.header-links li { - display: flex; - align-items: center; - - box-sizing: border-box; - height: 42px; - padding: 6px 0 0 0; - margin: 0; - - list-style: none; - white-space: nowrap; + display: flex; + align-items: center; + + box-sizing: border-box; + height: 42px; + padding: 6px 0 0 0; + margin: 0; + + list-style: none; + white-space: nowrap; } ul.header-links a { - display: flex; - align-items: center; - - color: var(--header-links-color); - font-family: var(--font-primary); - font-weight: 500; - font-size: 14px; - line-height: 1em; - text-decoration-color: var(--background-color-contrast-more); - text-decoration: none; + display: flex; + align-items: center; + + color: var(--header-links-color); + font-family: var(--font-primary); + font-weight: 500; + font-size: 14px; + line-height: 1em; + text-decoration-color: var(--background-color-contrast-more); + text-decoration: none; } ul.header-links a.external { - padding-right: 20px; + padding-right: 20px; } ul.header-links a.external:after { - content: ''; - display: block; - flex: 16px 0 0; - height: 16px; - margin: 0 0 0 4px; - background-color: currentColor; - mask: var(--icon-new-tab); - -webkit-mask: var(--icon-new-tab); - mask-size: 14px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 14px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; + content: ""; + display: block; + flex: 16px 0 0; + height: 16px; + margin: 0 0 0 4px; + background-color: currentColor; + mask: var(--icon-new-tab); + -webkit-mask: var(--icon-new-tab); + mask-size: 14px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 14px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; } - - /***** Primary parts *****/ @@ -458,659 +557,657 @@ section.parts-primary { } div.graphBox { - position: relative; - top: 0; - z-index: 1; - - display: flex; - flex-direction: column; - height: auto; - padding-bottom: 16px; - - background-color: var(--background-color-graph); - border-bottom: 1px solid var(--background-color-contrast); + position: relative; + top: 0; + z-index: 1; + + display: flex; + flex-direction: column; + height: auto; + padding-bottom: 16px; + + background-color: var(--background-color-graph); + border-bottom: 1px solid var(--background-color-contrast); } div.graphBox[data-sticky-graph="true"] { - position: sticky; + position: sticky; } div.graph-sizer { - position: relative; - - order: 2; - - flex: auto 0; - width: 100%; - max-width: calc(50vh * 2.2); - max-width: calc( (100vh - 300px) * 2.2 ); - margin: 16px auto 0 auto; -} + position: relative; + order: 2; + flex: auto 0; + width: 100%; + max-width: calc(50vh * 2.2); + max-width: calc((100vh - 300px) * 2.2); + margin: 16px auto 0 auto; +} /***** Tools styles *****/ div.tools { - order: 1; - - display: flex; - align-items: flex-start; - - box-sizing: border-box; - flex: auto 0 0; - max-height: 69px; - padding: 16px 0; - - overflow-x: auto; - - background-color: var(--background-color); - border-bottom: 1px solid var(--background-color-contrast); + order: 1; + + display: flex; + align-items: flex-start; + + box-sizing: border-box; + flex: auto 0 0; + max-height: 69px; + padding: 16px 0; + + overflow-x: auto; + + background-color: var(--background-color); + border-bottom: 1px solid var(--background-color-contrast); - overflow-y: hidden; + overflow-y: hidden; } /* Expand / Collapse */ div.expand-collapse { - position: sticky; - right: 0; - order: 10; - - display: none; - - padding: 0 16px; - margin-left: auto; - - background-color: var(--background-color); - border-left: 1px solid var(--background-color-contrast-more); + position: sticky; + right: 0; + order: 10; + + display: none; + + padding: 0 16px; + margin-left: auto; + + background-color: var(--background-color); + border-left: 1px solid var(--background-color-contrast-more); } body[data-expandable="true"] div.expand-collapse, body[data-expandable="only"] div.expand-collapse { - display: inherit; + display: inherit; } button#expand-collapse { - position: relative; - - display: block; - width: 36px; - height: 36px; - padding: 0; - - background-color: var(--background-color); - border: 1px solid var(--background-color-contrast); - border-radius: 4px; - outline: none; - - cursor: pointer; + position: relative; + + display: block; + width: 36px; + height: 36px; + padding: 0; + + background-color: var(--background-color); + border: 1px solid var(--background-color-contrast); + border-radius: 4px; + outline: none; + + cursor: pointer; } button#expand-collapse:before { - position: absolute; - top: 0; - left: 0; - - content: ''; - - box-sizing: border-box; - display: block; - width: 34px; - height: 34px; - background-color: var(--background-color-contrast-more); - - mask: var(--icon-expand); - -webkit-mask: var(--icon-expand); - mask-size: 20px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 20px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; - - pointer-events: none; + position: absolute; + top: 0; + left: 0; + + content: ""; + + box-sizing: border-box; + display: block; + width: 34px; + height: 34px; + background-color: var(--background-color-contrast-more); + + mask: var(--icon-expand); + -webkit-mask: var(--icon-expand); + mask-size: 20px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 20px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + + pointer-events: none; } body[data-graph-frame="expanded"] button#expand-collapse { -/* background-color: var(--background-color);*/ + /* background-color: var(--background-color);*/ } body[data-graph-frame="expanded"] button#expand-collapse:before { -/* background-color: var(--font-color-primary);*/ + /* background-color: var(--font-color-primary);*/ - mask: var(--icon-collapse); - -webkit-mask: var(--icon-collapse); - mask-size: 20px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 20px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; + mask: var(--icon-collapse); + -webkit-mask: var(--icon-collapse); + mask-size: 20px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 20px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; } /* Copy URL */ div.copy-url { - display: flex; - - padding: 0 0 0 16px; - margin: 0 16px 0 0; + display: flex; + + padding: 0 0 0 16px; + margin: 0 16px 0 0; } button#copy-url, button#download-faux { - position: relative; - - box-sizing: border-box; - padding: 11px 16px; - margin: 0; - overflow: hidden; - - background-color: var(--background-color); - border: 1px solid var(--background-color-contrast-more) !important; - border: none; - border-radius: 6px; - - color: var(--accent-color-contrast); - font-family: var(--font-primary); - font-weight: 400; - font-size: 12px; - line-height: 1em; - - white-space: nowrap; - cursor: pointer; - outline: none; + position: relative; + + box-sizing: border-box; + padding: 11px 16px; + margin: 0; + overflow: hidden; + + background-color: var(--background-color); + border: 1px solid var(--background-color-contrast-more) !important; + border: none; + border-radius: 6px; + + color: var(--accent-color-contrast); + font-family: var(--font-primary); + font-weight: 400; + font-size: 12px; + line-height: 1em; + + white-space: nowrap; + cursor: pointer; + outline: none; } button#copy-url:active, button#download-faux:active { - box-sizing: border-box; - background-color: var(--accent-color) !important; - border-color: var(--accent-color) !important; - - color: var(--font-color-secondary); + box-sizing: border-box; + background-color: var(--accent-color) !important; + border-color: var(--accent-color) !important; + + color: var(--font-color-secondary); } button#copy-url:before { - position: absolute; - top: 0; - left: 0; - - content: '👍'; - display: flex; - justify-content: center; - align-items: center; - - width: 100%; - height: 100%; - - background-color: var(--accent-color); - - color: var(--font-color-secondary); - - opacity: 0; - - transition: opacity 0.2s ease-out; + position: absolute; + top: 0; + left: 0; + + content: "👍"; + display: flex; + justify-content: center; + align-items: center; + + width: 100%; + height: 100%; + + background-color: var(--accent-color); + + color: var(--font-color-secondary); + + opacity: 0; + + transition: opacity 0.2s ease-out; } button#copy-url.clicked:before { - opacity: 1.0; - transition: opacity 0.0s linear; + opacity: 1; + transition: opacity 0s linear; } button#download-faux { - margin-left: 6px; + margin-left: 6px; } /* Zoom */ div.zoom { - order: 4; - - display: flex; - align-items: center; - - padding: 0 16px; - margin-left: 16px; - border-left: 1px solid var(--background-color-contrast-more); + order: 4; + + display: flex; + align-items: center; + + padding: 0 16px; + margin-left: 16px; + border-left: 1px solid var(--background-color-contrast-more); } div.zoom > span { - margin-right: 10px; + margin-right: 10px; - color: var(--background-color-contrast-more); - font-family: var(--font-secondary); - font-size: 11px; - line-height: 1em; - text-transform: uppercase; + color: var(--background-color-contrast-more); + font-family: var(--font-secondary); + font-size: 11px; + line-height: 1em; + text-transform: uppercase; } div.zoom button { - order: 1; - - padding: 11px 16px; - margin: 0; - - background-color: var(--background-color); - border: 1px solid var(--background-color-contrast-more); - outline: none; - - color: var(--accent-color-contrast); - font-family: var(--font-primary); - font-weight: 400; - font-size: 12px; - line-height: 1em; - - white-space: nowrap; - cursor: pointer; + order: 1; + + padding: 11px 16px; + margin: 0; + + background-color: var(--background-color); + border: 1px solid var(--background-color-contrast-more); + outline: none; + + color: var(--accent-color-contrast); + font-family: var(--font-primary); + font-weight: 400; + font-size: 12px; + line-height: 1em; + + white-space: nowrap; + cursor: pointer; } div.zoom button.selected { - background-color: var(--accent-color); - border-color: var(--accent-color); - - color: var(--font-color-secondary); + background-color: var(--accent-color); + border-color: var(--accent-color); + + color: var(--font-color-secondary); } div.zoom button:nth-of-type(1) { - border-right: none; - border-radius: 6px 0 0 6px; + border-right: none; + border-radius: 6px 0 0 6px; } div.zoom button:nth-of-type(2) { - border-left: none; - border-right: none; + border-left: none; + border-right: none; } div.zoom button:nth-last-of-type(1) { - border-left: none; - border-radius: 0 6px 6px 0; + border-left: none; + border-radius: 0 6px 6px 0; } /* Normalize */ div.normalize { - order: 2; - - display: flex; - align-items: center; - - padding: 0 0 0 16px; - - border-left: 1px solid var(--background-color-contrast-more); + order: 2; + + display: flex; + align-items: center; + + padding: 0 0 0 16px; + + border-left: 1px solid var(--background-color-contrast-more); } div.normalize > span { - margin-right: 10px; + margin-right: 10px; - color: var(--background-color-contrast-more); - font-family: var(--font-secondary); - font-size: 11px; - line-height: 1em; - text-transform: uppercase; + color: var(--background-color-contrast-more); + font-family: var(--font-secondary); + font-size: 11px; + line-height: 1em; + text-transform: uppercase; } div.normalize .helptip { - display: none; + display: none; } div.normalize > div { - display: flex; - align-items: center; + display: flex; + align-items: center; } div.normalize > div + div { - margin-left: 6px; + margin-left: 6px; } div.normalize > div > input { - order: 2; - - box-sizing: border-box; - width: 70px; - height: 36px; - padding: 10px 0; - - background-color: var(--background-color-inputs); - border: 1px solid var(--background-color-contrast-more); - border-right: none; - border-left: none; - border-radius: 0px; - outline: none; - - color: var(--font-color-inputs); - font-family: var(--font-secondary); - font-size: 11px; - line-height: 1em; - text-transform: uppercase; - text-align: center; + order: 2; + + box-sizing: border-box; + width: 70px; + height: 36px; + padding: 10px 0; + + background-color: var(--background-color-inputs); + border: 1px solid var(--background-color-contrast-more); + border-right: none; + border-left: none; + border-radius: 0px; + outline: none; + + color: var(--font-color-inputs); + font-family: var(--font-secondary); + font-size: 11px; + line-height: 1em; + text-transform: uppercase; + text-align: center; } div.normalize > div:after { - order: 3; - content: ''; - - box-sizing: border-box; - display: block; - width: 6px; - height: 36px; - - background-color: var(--background-color-inputs); - border: 1px solid var(--background-color-contrast-more); - border-left: none; - border-radius: 0 6px 6px 0; + order: 3; + content: ""; + + box-sizing: border-box; + display: block; + width: 6px; + height: 36px; + + background-color: var(--background-color-inputs); + border: 1px solid var(--background-color-contrast-more); + border-left: none; + border-radius: 0 6px 6px 0; } div.normalize > div > span { - order: 1; - - padding: 11px 16px; - - background-color: var(--background-color) !important; - border: 1px solid var(--background-color-contrast-more); - border-right: none; - border-radius: 6px 0 0 6px; - - color: var(--accent-color-contrast); - font-weight: 400; - font-size: 12px; - line-height: 1em; - - white-space: nowrap; - cursor: pointer; + order: 1; + + padding: 11px 16px; + + background-color: var(--background-color) !important; + border: 1px solid var(--background-color-contrast-more); + border-right: none; + border-radius: 6px 0 0 6px; + + color: var(--accent-color-contrast); + font-weight: 400; + font-size: 12px; + line-height: 1em; + + white-space: nowrap; + cursor: pointer; } div.normalize > div.selected > span { - background-color: var(--accent-color) !important; - border-color: var(--accent-color); - - color: var(--font-color-secondary); + background-color: var(--accent-color) !important; + border-color: var(--accent-color); + + color: var(--font-color-secondary); } /* Smooth */ div.smooth { - order: 3; - - display: flex; - align-items: center; - - padding-left: 16px; - margin-left: 16px; - border-left: 1px solid var(--background-color-contrast-more); + order: 3; + + display: flex; + align-items: center; + + padding-left: 16px; + margin-left: 16px; + border-left: 1px solid var(--background-color-contrast-more); } div.smooth > span { - margin-right: 10px; + margin-right: 10px; - color: var(--background-color-contrast-more); - font-family: var(--font-secondary); - font-size: 11px; - line-height: 1em; - text-transform: uppercase; + color: var(--background-color-contrast-more); + font-family: var(--font-secondary); + font-size: 11px; + line-height: 1em; + text-transform: uppercase; } div.smooth input { - box-sizing: border-box; - width: 70px; - height: 36px; - padding: 10px 0; - - background-color: var(--background-color-inputs); - border: 1px solid var(--background-color-contrast-more); - border-right: none; - border-radius: 6px 0 0 6px; - outline: none; - - color: var(--font-color-inputs); - font-family: var(--font-secondary); - font-size: 11px; - line-height: 1em; - text-transform: uppercase; - text-align: center; + box-sizing: border-box; + width: 70px; + height: 36px; + padding: 10px 0; + + background-color: var(--background-color-inputs); + border: 1px solid var(--background-color-contrast-more); + border-right: none; + border-radius: 6px 0 0 6px; + outline: none; + + color: var(--font-color-inputs); + font-family: var(--font-secondary); + font-size: 11px; + line-height: 1em; + text-transform: uppercase; + text-align: center; } div.smooth:after { - order: 3; - content: ''; - - box-sizing: border-box; - display: block; - width: 6px; - height: 36px; - - background-color: var(--background-color-inputs); - border: 1px solid var(--background-color-contrast-more); - border-left: none; - border-radius: 0 6px 6px 0; + order: 3; + content: ""; + + box-sizing: border-box; + display: block; + width: 6px; + height: 36px; + + background-color: var(--background-color-inputs); + border: 1px solid var(--background-color-contrast-more); + border-left: none; + border-radius: 0 6px 6px 0; } /* Download, Inspect, Label, Recolor */ div.miscTools { - order: 4; - - display: flex; - - background-color: var(--background-color); - border-left: 1px solid var(--background-color-contrast-more); - - padding: 0 16px; + order: 4; + + display: flex; + + background-color: var(--background-color); + border-left: 1px solid var(--background-color-contrast-more); + + padding: 0 16px; } div.miscTools button, div.extra-panel button { - order: 1; - - flex: auto 1 1; - padding: 11px 8px; - margin: 0; - - background-color: var(--background-color) !important; - border: 1px solid var(--background-color-contrast-more); - border-radius: 6px; - - color: var(--accent-color-contrast); - font-family: var(--font-primary); - font-weight: 400; - font-size: 12px; - line-height: 1em; - text-transform: capitalize; - - white-space: nowrap; - cursor: pointer; - outline: none; + order: 1; + + flex: auto 1 1; + padding: 11px 8px; + margin: 0; + + background-color: var(--background-color) !important; + border: 1px solid var(--background-color-contrast-more); + border-radius: 6px; + + color: var(--accent-color-contrast); + font-family: var(--font-primary); + font-weight: 400; + font-size: 12px; + line-height: 1em; + text-transform: capitalize; + + white-space: nowrap; + cursor: pointer; + outline: none; } div.filters-button { - display: flex; - flex-wrap: wrap; - gap: 0 4px; + display: flex; + flex-wrap: wrap; + gap: 0 4px; } div.extra-panel button.sort-filters, div.extra-panel button.readme { - flex: calc(50% - 2px) 0 0; - border-color: var(--background-color-contrast) !important; + flex: calc(50% - 2px) 0 0; + border-color: var(--background-color-contrast) !important; } div.extra-panel button.import-filters, div.extra-panel button.export-filters { - position: relative; - - flex: 100% 0 0; + position: relative; + + flex: 100% 0 0; } button.export-graphic-filters { - border-color: var(--background-color-contrast) !important; + border-color: var(--background-color-contrast) !important; } button.export-filters:after { - content: ''; - - position: absolute; - top: 0; - right: 0; - content: ''; - box-sizing: border-box; - display: block; - width: 34px; - height: 34px; - background-color: var(--background-color-contrast-more); - pointer-events: none; - - mask: var(--icon-download); - -webkit-mask: var(--icon-download); - mask-size: 20px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 20px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; + content: ""; + + position: absolute; + top: 0; + right: 0; + content: ""; + box-sizing: border-box; + display: block; + width: 34px; + height: 34px; + background-color: var(--background-color-contrast-more); + pointer-events: none; + + mask: var(--icon-download); + -webkit-mask: var(--icon-download); + mask-size: 20px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 20px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; } div.settings-row { - display: flex; - align-items: center; - gap: 4px; + display: flex; + align-items: center; + gap: 4px; } div.settings-row > span { - flex: calc(25% - 4px) 0 0; + flex: calc(25% - 4px) 0 0; } div.settings-row > span:nth-child(1) { - flex: auto 1 1; - - font-size: 12px; - line-height: 1em; - text-align: left; + flex: auto 1 1; + + font-size: 12px; + line-height: 1em; + text-align: left; } div.miscTools button span { - display: none; + display: none; } div.miscTools button + button { - margin-left: 6px; + margin-left: 6px; } div.miscTools button.selected { - background-color: var(--accent-color) !important; - border-color: var(--accent-color); - - color: var(--font-color-secondary); + background-color: var(--accent-color) !important; + border-color: var(--accent-color); + + color: var(--font-color-secondary); } div.miscTools button#recolor:active { - background-color: var(--accent-color) !important; - border-color: var(--accent-color); - - color: var(--font-color-secondary); + background-color: var(--accent-color) !important; + border-color: var(--accent-color); + + color: var(--font-color-secondary); } div.tools div.miscTools button:not(#download) { - display: inherit; + display: inherit; } div.miscTools button#download { - display: none; - - order: 0; - margin: 0 6px 0 0; + display: none; + + order: 0; + margin: 0 6px 0 0; } div.miscTools button#download:active { - background-color: var(--accent-color) !important; - - color: var(--font-color-secondary); + background-color: var(--accent-color) !important; + + color: var(--font-color-secondary); } svg#expandTools { - display: none; + display: none; } svg#expandTools path { - display: none; + display: none; } button#theme { - justify-content: center; - min-width: 120px; + justify-content: center; + min-width: 120px; } body.dark-mode div.miscTools button#theme, div.miscTools button#theme:active { - background-color: var(--accent-color) !important; - border-color: var(--accent-color); + background-color: var(--accent-color) !important; + border-color: var(--accent-color); - color: var(--font-color-secondary); + color: var(--font-color-secondary); } /** extra panel **/ div.extra-panel { - flex-direction: column; - overflow: auto; + flex-direction: column; + overflow: auto; } div.extra-panel > div { - margin: 0 0 1em 0; + margin: 0 0 1em 0; } div.extra-panel h5 { - margin: 16px 0 8px 0; - - font-size: 14px; - line-height: 1em; - color: var(--accent-color-contrast); + margin: 16px 0 8px 0; + + font-size: 14px; + line-height: 1em; + color: var(--accent-color-contrast); } div.extra-upload { - display: flex; - flex-wrap: wrap; + display: flex; + flex-wrap: wrap; } div.extra-upload h5 { - flex: 100% 0 0; + flex: 100% 0 0; } div.extra-upload button { - flex: calc(50% - 2px) 1 1; + flex: calc(50% - 2px) 1 1; } div.extra-upload button + button { - margin-left: 4px; + margin-left: 4px; } div.extra-upload span { - order: 2; - flex: 100% 0 0; + order: 2; + flex: 100% 0 0; } div.extra-panel span { - color: var(--background-color-contrast-more); + color: var(--background-color-contrast-more); } div.extra-panel select { - position: relative; - - padding: 10px 0; - margin: 0; - - background-color: transparent; - border: none; - - color: var(--font-color-primary); - font-family: var(--font-family-secondary); - font-size: 11px; - line-height: 1em; - - cursor: pointer; - outline: none; + position: relative; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - -webkit-appearance: none; - -moz-appearance: none; + padding: 10px 0; + margin: 0; + + background-color: transparent; + border: none; + + color: var(--font-color-primary); + font-family: var(--font-family-secondary); + font-size: 11px; + line-height: 1em; + + cursor: pointer; + outline: none; + + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-appearance: none; + -moz-appearance: none; } div.extra-panel select { @@ -1135,1898 +1232,1880 @@ div.extra-panel select[name="phone"] { div.extra-panel > div.extra-eq > div.select-eq-phone, div.extra-panel > div.extra-eq > div.filters, div.extra-panel div.settings-row { - margin: 0 0 0.5em 0; + margin: 0 0 0.5em 0; } div.extra-panel > div.extra-eq > div.select-eq-phone > select { - box-sizing: border-box; - width: 100%; - padding: 10px; - - border: 1px solid var(--background-color-contrast-more); - border-radius: 6px; + box-sizing: border-box; + width: 100%; + padding: 10px; + + border: 1px solid var(--background-color-contrast-more); + border-radius: 6px; } div.extra-panel > div.extra-eq > div.filters-header > span, div.extra-panel > div.extra-eq > div.filters > div.filter > span { - flex: 25% 1 1; - display: flex; - align-items: center; + flex: 25% 1 1; + display: flex; + align-items: center; } -div.extra-panel > div.extra-eq > div.filters > div.filter > span > input[type='checkbox'] { - width: 20%; - margin: 0; - -webkit-appearance: checkbox; - appearance: checkbox; - - accent-color: var(--background-color-contrast); - - cursor: pointer; +div.extra-panel > div.extra-eq > div.filters > div.filter > span > input[type="checkbox"] { + width: 20%; + margin: 0; + -webkit-appearance: checkbox; + appearance: checkbox; + + accent-color: var(--background-color-contrast); + + cursor: pointer; } -div.extra-panel > div.extra-eq > div.filters > div.filter > span > input[type='checkbox']:checked { +div.extra-panel > div.extra-eq > div.filters > div.filter > span > input[type="checkbox"]:checked { } div.extra-panel > div.extra-eq > div.filters > div.filter > span > select { - width: 70%; + width: 70%; } div.extra-panel div.settings-row > span { - width: 30%; - display: inline-block; + width: 30%; + display: inline-block; } div.extra-panel > div.extra-eq > div.filters > div.filter > span > input, div.extra-panel div.settings-row > span > input { - box-sizing: border-box; - width: 100%; - padding: 6px 10px; - - background-color: var(--background-color-inputs); - border: 1px solid var(--background-color-contrast); - border-radius: 6px; - - color: var(--font-color-inputs); - font-family: var(--font-secondary); - font-size: 11px; - line-height: 12px; - - outline: none; + box-sizing: border-box; + width: 100%; + padding: 6px 10px; + + background-color: var(--background-color-inputs); + border: 1px solid var(--background-color-contrast); + border-radius: 6px; + + color: var(--font-color-inputs); + font-family: var(--font-secondary); + font-size: 11px; + line-height: 12px; + + outline: none; } span:has(input[name="freq"]), span:has(input[name="gain"]), span:has(input[name="autoeq-to"]), span:has(input[name="tone-generator-to"]) { - position: relative; + position: relative; } span:has(input[name="freq"]):after, span:has(input[name="gain"]):after, span:has(input[name="autoeq-to"]):after, span:has(input[name="tone-generator-to"]):after { - position: absolute; - top: 1px; - right: 1px; - - content: 'Hz'; - - box-sizing: border-box; - padding: 6px 10px 6px 0; - - border-left: none; - border-radius: 0 6px 6px 0; - - font-family: var(--font-secondary); - font-size: 11px; - line-height: 11px; - - pointer-events: none; + position: absolute; + top: 1px; + right: 1px; + + content: "Hz"; + + box-sizing: border-box; + padding: 6px 10px 6px 0; + + border-left: none; + border-radius: 0 6px 6px 0; + + font-family: var(--font-secondary); + font-size: 11px; + line-height: 11px; + + pointer-events: none; } span:has(input[name="gain"]):after { - content: 'dB'; + content: "dB"; } div.filters-header { - display: flex; - gap: 4px; - margin: 8px 0; - - font-size: 12px; - line-height: 1em; + display: flex; + gap: 4px; + margin: 8px 0; + + font-size: 12px; + line-height: 1em; } div.filter { - box-sizing: border-box; - display: flex; - gap: 4px; + box-sizing: border-box; + display: flex; + gap: 4px; } div.extra-eq-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 1000; - background-color: rgba(0, 0, 0, 0.7); - color: #fff; - text-align: center; - padding: calc(50vh - 1em) 0; - font-weight: bold; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; + background-color: rgba(0, 0, 0, 0.7); + color: #fff; + text-align: center; + padding: calc(50vh - 1em) 0; + font-weight: bold; } div.extra-panel button { - margin-bottom: 4px !important; + margin-bottom: 4px !important; } div.extra-panel div:has(> button.play) { - display: flex; - align-items: center; - - margin: 8px 0 0 0; + display: flex; + align-items: center; + + margin: 8px 0 0 0; } div.extra-panel button.play { - order: 0; - flex: calc(50% - 2px) 0 0; + order: 0; + flex: calc(50% - 2px) 0 0; } div.extra-panel button.play + span { - flex: auto 1 1; - margin: 0 0 0 10px; - - font-size: 12px; - line-height: 1em; + flex: auto 1 1; + margin: 0 0 0 10px; + + font-size: 12px; + line-height: 1em; } -div.extra-panel > div.extra-tone-generator input[name='tone-generator-freq'] { - box-sizing: border-box; - width: 100%; - - background-color: var(--background-color-inputs); - border-radius: 100px; - - accent-color: var(--accent-color-contrast); - - cursor: pointer; - +div.extra-panel > div.extra-tone-generator input[name="tone-generator-freq"] { + box-sizing: border-box; + width: 100%; + + background-color: var(--background-color-inputs); + border-radius: 100px; + + accent-color: var(--accent-color-contrast); + + cursor: pointer; } /***** Targets styles *****/ div.targets { - display: flex; - align-items: flex-start; - - box-sizing: border-box; - padding: 0 16px 16px 16px; - padding: 16px; - max-height: 68px; - - overflow-x: auto; - overflow-y: hidden; + display: flex; + align-items: flex-start; + + box-sizing: border-box; + padding: 0 16px 16px 16px; + padding: 16px; + max-height: 68px; + + overflow-x: auto; + overflow-y: hidden; } div.targets > div.targetLabel { - display: none; + display: none; } .targetClass { - flex: auto 0 0 !important; + flex: auto 0 0 !important; } .targetClass + .targetClass { - padding-left: 16px; - padding-right: 16px; - margin-left: 16px; - border-left: 1px solid var(--background-color-contrast-more); + padding-left: 16px; + padding-right: 16px; + margin-left: 16px; + border-left: 1px solid var(--background-color-contrast-more); } .targetClass div.targetLabel { - margin-right: 10px; + margin-right: 10px; - color: var(--background-color-contrast-more); - font-family: var(--font-secondary); - font-size: 11px; - line-height: 1em; - text-transform: uppercase; + color: var(--background-color-contrast-more); + font-family: var(--font-secondary); + font-size: 11px; + line-height: 1em; + text-transform: uppercase; } .targetClass div.targetLabel:after { - content: ' targets:'; - white-space: pre; + content: " targets:"; + white-space: pre; } div.targets .targetClass { - display: flex; + display: flex; } div.targetLabel { - display: flex; - align-items: center; + display: flex; + align-items: center; } div.target { - padding: 11px 16px; - - background-color: var(--background-color) !important; - border: 1px solid var(--background-color-contrast-more); - border-radius: 6px; - - color: var(--accent-color-contrast); - font-weight: 400; - font-size: 12px; - line-height: 1em; - - white-space: nowrap; - cursor: pointer; + padding: 11px 16px; + + background-color: var(--background-color) !important; + border: 1px solid var(--background-color-contrast-more); + border-radius: 6px; + + color: var(--accent-color-contrast); + font-weight: 400; + font-size: 12px; + line-height: 1em; + + white-space: nowrap; + cursor: pointer; } div.target + .target { - margin-left: 6px; + margin-left: 6px; } div.target[style*="background"] { - background-color: var(--accent-color) !important; - border-color: var(--accent-color) !important; - - color: var(--font-color-secondary); -} - + background-color: var(--accent-color) !important; + border-color: var(--accent-color) !important; + color: var(--font-color-secondary); +} /***** Table styles *****/ table.manageTable { - display: flex; - flex-direction: column; - width: 100%; + display: flex; + flex-direction: column; + width: 100%; } table.manageTable td { - padding: 0; + padding: 0; } tbody.curves:empty { - display: flex; - justify-content: center; - align-items: center; - - min-height: 50px; - - border-top: 1px solid var(--background-color-contrast); + display: flex; + justify-content: center; + align-items: center; + + min-height: 50px; + + border-top: 1px solid var(--background-color-contrast); } tbody.curves:empty:before { - content: 'Select a model from the list to the left to graph its frequency response'; - - padding: 14px 16px; - - font-size: 14px; - line-height: 1.6em; - text-align: center; + content: "Select a model from the list to the left to graph its frequency response"; + + padding: 14px 16px; + + font-size: 14px; + line-height: 1.6em; + text-align: center; } tbody.curves > tr { - display: flex; - align-items: flex-start; - - border-top: 1px solid var(--background-color-contrast); + display: flex; + align-items: flex-start; + + border-top: 1px solid var(--background-color-contrast); } tbody.curves > tr > td { - position: relative; - - display: flex; - justify-content: center; - align-items: center; - - flex: 50px 0 0; - box-sizing: border-box; - min-height: 50px; + position: relative; + + display: flex; + justify-content: center; + align-items: center; + + flex: 50px 0 0; + box-sizing: border-box; + min-height: 50px; } tbody.curves > tr > td.button, tbody.curves > tr > td.button-pin, tbody.curves > tr > td.remove { - box-sizing: border-box; - flex: 36px 0 0; - margin-left: 6px; - - cursor: pointer; + box-sizing: border-box; + flex: 36px 0 0; + margin-left: 6px; + + cursor: pointer; } /* Table buttons */ tbody.curves > tr > td.button:before, tbody.curves > tr > td.button-pin:before, tbody.curves > tr > td.remove:before { - position: absolute; - - top: 6px; - left: 0; - - content: ''; - box-sizing: border-box; - display: block; - width: 36px; - height: 36px; - - background-color: var(--background-color); - border: 1px solid var(--background-color-contrast); - border-radius: 6px; - - pointer-events: none; + position: absolute; + + top: 6px; + left: 0; + + content: ""; + box-sizing: border-box; + display: block; + width: 36px; + height: 36px; + + background-color: var(--background-color); + border: 1px solid var(--background-color-contrast); + border-radius: 6px; + + pointer-events: none; } tbody.curves > tr > td.button:after, tbody.curves > tr > td.button-pin:after, tbody.curves > tr > td.remove:after { - position: absolute; - - top: 6px; - left: 0px; - - content: ''; - box-sizing: border-box; - display: block; - width: 36px; - height: 36px; - - background-color: var(--background-color-contrast-more); - - pointer-events: none; + position: absolute; + + top: 6px; + left: 0px; + + content: ""; + box-sizing: border-box; + display: block; + width: 36px; + height: 36px; + + background-color: var(--background-color-contrast-more); + + pointer-events: none; } /* Remove item */ tbody.curves > tr > td.remove { - order: 7; - - margin: 0 16px 0 6px; - - background: none !important; + order: 7; + + margin: 0 16px 0 6px; + + background: none !important; } tbody.curves > tr > td.remove:after { - mask: var(--icon-remove); - -webkit-mask: var(--icon-remove); - - mask-size: 20px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 20px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; + mask: var(--icon-remove); + -webkit-mask: var(--icon-remove); + + mask-size: 20px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 20px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; } tbody.curves > tr > td.remove svg { - display: none; + display: none; } tbody.curves > tr > td.button-pin:after { - mask: var(--icon-pin); - -webkit-mask: var(--icon-pin); - - mask-size: 20px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 20px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; + mask: var(--icon-pin); + -webkit-mask: var(--icon-pin); + + mask-size: 20px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 20px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; } tbody.curves > tr > td.button.hideIcon:after { - mask: var(--icon-hide); - -webkit-mask: var(--icon-hide); - - mask-size: 20px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 20px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; + mask: var(--icon-hide); + -webkit-mask: var(--icon-hide); + + mask-size: 20px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 20px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; } tbody.curves > tr > td.button.button-export:after { - mask: var(--icon-download); - -webkit-mask: var(--icon-download); - - mask-size: 20px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 20px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; + mask: var(--icon-download); + -webkit-mask: var(--icon-download); + + mask-size: 20px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 20px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; } tbody.curves > tr > td.button.button-baseline:after { - mask: var(--icon-squiggle); - -webkit-mask: var(--icon-squiggle); - - mask-size: 20px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 20px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; + mask: var(--icon-squiggle); + -webkit-mask: var(--icon-squiggle); + + mask-size: 20px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 20px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; } tbody.curves > tr > td.button.button-baseline.selected:after { - mask: var(--icon-baseline); - -webkit-mask: var(--icon-baseline); - - mask-size: 20px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 20px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; + mask: var(--icon-baseline); + -webkit-mask: var(--icon-baseline); + + mask-size: 20px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 20px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; } /* Baseline */ tbody.curves > tr > td.button-baseline { - position: relative; - - order: 3; + position: relative; + + order: 3; } tbody.curves > tr > td.button-baseline:before { - background-color: var(--background-color); - border-color: transparent; + background-color: var(--background-color); + border-color: transparent; } tbody.curves > tr > td.button-baseline:after { - background-color: var(--font-color-primary); + background-color: var(--font-color-primary); } /* Download */ tbody.curves > tr > td.button-export { - position: relative; - - background-color: transparent; - - order: 6; + position: relative; + + background-color: transparent; + + order: 6; } tbody.curves > tr > td.button-export:before { - background-color: var(--background-color); + background-color: var(--background-color); } tbody.curves > tr > td.button-export:after { - background-color: var(--background-color-contrast-more); + background-color: var(--background-color-contrast-more); } /* Hide */ tbody.curves > tr > td.hideIcon { - position: relative; - - order: 4; + position: relative; + + order: 4; } tbody.curves > tr > td.hideIcon svg { - display: none; + display: none; } tbody.curves > tr > td.hideIcon:before { - background-color: var(--background-color); - border-color: transparent; + background-color: var(--background-color); + border-color: transparent; } tbody.curves > tr > td.hideIcon:after { - background-color: var(--font-color-primary); + background-color: var(--font-color-primary); } tbody.curves > tr > td.hideIcon.selected:before { - border-color: var(--background-color-contrast); + border-color: var(--background-color-contrast); } tbody.curves > tr > td.hideIcon.selected:after { - background-color: var(--background-color-contrast-more); + background-color: var(--background-color-contrast-more); } /* Pin */ tbody.curves > tr > td.button-pin { - position: relative; - - order: 5; + position: relative; + + order: 5; } tbody.curves > tr > td.button-pin[data-pinned="true"]:before { - background-color: var(--background-color); - border-color: transparent; + background-color: var(--background-color); + border-color: transparent; } tbody.curves > tr > td.button-pin[data-pinned="true"]:after { - background-color: var(--font-color-primary); + background-color: var(--font-color-primary); } tbody.curves > tr > td.button-pin svg { - display: none; + display: none; } /* Curve color */ tbody.curves > tr > td.curve-color { - order: 0; - align-self: flex-start; - - display: flex; - box-sizing: border-box; - justify-content: flex-end; - align-items: center; - flex: 38px 0 0; - min-height: 50px; - - cursor: pointer; + order: 0; + align-self: flex-start; + + display: flex; + box-sizing: border-box; + justify-content: flex-end; + align-items: center; + flex: 38px 0 0; + min-height: 50px; + + cursor: pointer; } tbody.curves > tr > td.curve-color button { - box-sizing: border-box; - flex: 16px 0 0; - height: 16px; - padding: 0; - margin: 0 6px 0 0; - - border: none; - border-radius: 8px; - outline: none; - - color: inherit; - background-color: currentColor; - - cursor: pointer; + box-sizing: border-box; + flex: 16px 0 0; + height: 16px; + padding: 0; + margin: 0 6px 0 0; + + border: none; + border-radius: 8px; + outline: none; + + color: inherit; + background-color: currentColor; + + cursor: pointer; } /* Model name item */ tbody.curves > tr > td.item-line { - order: 1; - - box-sizing: border-box; - flex: auto 1 1; - justify-content: flex-start; - align-items: center; - - display: flex; - - color: var(--background-color-contrast-more); - font-weight: 700; - cursor: inherit; + order: 1; + + box-sizing: border-box; + flex: auto 1 1; + justify-content: flex-start; + align-items: center; + + display: flex; + + color: var(--background-color-contrast-more); + font-weight: 700; + cursor: inherit; } tbody.curves > tr > td.item-line > span { - flex: auto 0 0; + flex: auto 0 0; } tbody.curves > tr > td.item-line.item-target > span:after { - content: ':'; + content: ":"; } tbody.curves > tr > td.item-line > span:nth-child(1), tbody.curves > tr > td.item-line > span + div { - align-self: flex-start; - - display: flex; - align-items: center; - min-height: 50px; - - color: var(--font-color-primary); + align-self: flex-start; + + display: flex; + align-items: center; + min-height: 50px; + + color: var(--font-color-primary); } tbody.curves > tr > td.item-line span { - order: 2; + order: 2; } tbody.curves > tr > td.item-line div.phonename { - order: 4; - display: flex; - flex-direction: row; - flex-wrap: wrap; - flex: auto 1 1; - padding: 0 6px 0 6px; + order: 4; + display: flex; + flex-direction: row; + flex-wrap: wrap; + flex: auto 1 1; + padding: 0 6px 0 6px; } tbody.curves > tr > td.item-line div.variantName { - position: relative; - top: auto !important; - - width: auto !important; - flex: calc(100% - 52px) 0 0; - height: 50px; - - box-sizing: border-box; - display: flex; - align-items: center; - margin: 0 16px 0 0; + position: relative; + top: auto !important; - color: var(--font-color-primary) !important; - font-weight: 400; - - cursor: pointer !important; + width: auto !important; + flex: calc(100% - 52px) 0 0; + height: 50px; + + box-sizing: border-box; + display: flex; + align-items: center; + margin: 0 16px 0 0; + + color: var(--font-color-primary) !important; + font-weight: 400; + + cursor: pointer !important; } tbody.curves > tr > td.item-line div.variantName:hover { - text-decoration: underline; + text-decoration: underline; } tbody.curves > tr > td.item-line div.variantName[style*="color"] { - font-weight: 700; + font-weight: 700; } tbody.curves > tr > td.item-line span.variantPopout { - position: relative; - top: 7px !important; - left: auto !important; - - box-sizing: border-box; - display: flex; - justify-content: center; - align-items: center; - flex: 36px 0 0; - height: 36px; - - background-color: var(--background-color); - border-radius: 6px; - - font-size: 0px; - font-weight: 400; - line-height: 1em; - - cursor: pointer; + position: relative; + top: 7px !important; + left: auto !important; + + box-sizing: border-box; + display: flex; + justify-content: center; + align-items: center; + flex: 36px 0 0; + height: 36px; + + background-color: var(--background-color); + border-radius: 6px; + + font-size: 0px; + font-weight: 400; + line-height: 1em; + + cursor: pointer; } tbody.curves > tr > td.item-line span.variantPopout[style*="display"] { - background-color: var(--background-color); + background-color: var(--background-color); } tbody.curves > tr > td.item-line span.variantPopout:after { - position: absolute; - - top: 0; - left: 0; - - content: ''; - box-sizing: border-box; - display: block; - width: 36px; - height: 36px; - - background-color: var(--background-color-contrast-more); - mask: var(--icon-plus); - -webkit-mask: var(--icon-plus); - - mask-size: 16px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 16px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; - - pointer-events: none; + position: absolute; + + top: 0; + left: 0; + + content: ""; + box-sizing: border-box; + display: block; + width: 36px; + height: 36px; + + background-color: var(--background-color-contrast-more); + mask: var(--icon-plus); + -webkit-mask: var(--icon-plus); + + mask-size: 16px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 16px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + + pointer-events: none; } tbody.curves > tr > td.item-line span.variantPopout[style*="display"]:after { - background-color: var(--font-color-primary); - display: none; + background-color: var(--font-color-primary); + display: none; } /* Styles for variantName + variantPopout layout */ div.variant-names { - flex: auto 1 1; - max-width: calc(100% - 40px); + flex: auto 1 1; + max-width: calc(100% - 40px); } div.variant-popouts { - flex: 36px 0 0; - padding: 7px 0 0 0; - padding: 0; + flex: 36px 0 0; + padding: 7px 0 0 0; + padding: 0; } span.variantPopout { - margin: 0 0 14px 0; - display: flex !important; + margin: 0 0 14px 0; + display: flex !important; } span.variantPopout[style*="display: none;"] { - opacity: 0.0; + opacity: 0; } /* -- */ tbody.curves > tr > td.item-line div.variants { - position: relative; - - order: 2; - align-self: flex-start; - - display: flex; - align-items: center; - flex: 20px 0 0; - height: 50px; - margin: 0 0 0 6px; - - border-radius: 50%; - - cursor: pointer; - outline: none; + position: relative; + + order: 2; + align-self: flex-start; + + display: flex; + align-items: center; + flex: 20px 0 0; + height: 50px; + margin: 0 0 0 6px; + + border-radius: 50%; + + cursor: pointer; + outline: none; } tbody.curves > tr > td.item-line div.variants:before { - content: ''; - - box-sizing: border-box; - display: flex; - justify-content: center; - align-items: center; - flex: 20px 0 0; - height: 20px; - - border: 1px solid currentColor; - border-radius: 50%; - - color: currentColor; - font-family: var(--font-secondary); - font-weight: 400; - font-size: 11px; - line-height: 1em; - text-transform: uppercase; + content: ""; + + box-sizing: border-box; + display: flex; + justify-content: center; + align-items: center; + flex: 20px 0 0; + height: 20px; + + border: 1px solid currentColor; + border-radius: 50%; + + color: currentColor; + font-family: var(--font-secondary); + font-weight: 400; + font-size: 11px; + line-height: 1em; + text-transform: uppercase; } tbody.curves > tr > td.item-line div.variants:after { -position: absolute; - top: 6px; - left: 3px; - - content: ''; - - box-sizing: border-box; - display: block; - width: calc(100% - 6px); - height: calc(100% - 12px); - - background-color: currentColor; - - mask: var(--icon-plus); - -webkit-mask: var(--icon-plus); - mask-size: 16px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 16px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; - - pointer-events: none; + position: absolute; + top: 6px; + left: 3px; + + content: ""; + + box-sizing: border-box; + display: block; + width: calc(100% - 6px); + height: calc(100% - 12px); + + background-color: currentColor; + + mask: var(--icon-plus); + -webkit-mask: var(--icon-plus); + mask-size: 16px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 16px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + + pointer-events: none; } tbody.curves > tr > td.item-line div.variants path { - display: none; + display: none; } /* L+R line w/ svg */ tbody.curves > tr > td.channels { - order: 2; - - flex: 100px 0 0; + order: 2; + + flex: 100px 0 0; } tbody.curves > tr > td.channels svg.keyLine { - height: 100px; - width: auto; - max-height: 40px; + height: 100px; + width: auto; + max-height: 40px; } tbody.curves > tr > td.channels text { - fill: var(--background-color-contrast-more) !important; - color: var(--background-color-contrast-more) !important; - font-family: var(--font-secondary); - font-size: 8px !important; - text-transform: uppercase; + fill: var(--background-color-contrast-more) !important; + color: var(--background-color-contrast-more) !important; + font-family: var(--font-secondary); + font-size: 8px !important; + text-transform: uppercase; } /* Shift graph input / Levels */ tbody.curves > tr > td.levels { - order: 2; - - padding: 0 0 0 16px; + order: 2; + + padding: 0 0 0 16px; } tbody.curves > tr > td.levels input { - box-sizing: border-box; - width: 70px; - height: 36px; - padding: 11px 6px 11px 0; - - background-color: var(--background-color-inputs); - border: 1px solid var(--background-color-contrast-more); - border-radius: 6px; - outline: none; - - color: var(--font-color-inputs); - font-family: var(--font-secondary); - font-size: 11px; - line-height: 1em; - text-transform: uppercase; - text-align: center; + box-sizing: border-box; + width: 70px; + height: 36px; + padding: 11px 6px 11px 0; + + background-color: var(--background-color-inputs); + border: 1px solid var(--background-color-contrast-more); + border-radius: 6px; + outline: none; + + color: var(--font-color-inputs); + font-family: var(--font-secondary); + font-size: 11px; + line-height: 1em; + text-transform: uppercase; + text-align: center; } /* List lock controls */ tr.addPhone { - display: none; + display: none; } - - /***** Secondary parts *****/ div.controls, div.select { - width: 100%; - height: 100%; - overflow: hidden; - - background-color: var(--background-color); - border-right: 1px solid var(--background-color-contrast-more); + width: 100%; + height: 100%; + overflow: hidden; + + background-color: var(--background-color); + border-right: 1px solid var(--background-color-contrast-more); } div.select { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } div.select > div.selector-panel { - width: 100%; - height: 100%; - overflow: hidden; - display: flex; - flex-direction: column; + width: 100%; + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; } input.search { - position: sticky; - top: 0; - - flex: auto 0 0; - - box-sizing: border-box; - height: 36px; - padding: 10px 16px; - margin: 16px 16px 0 16px; - - background-color: var(--background-color-inputs); - border: 1px solid var(--background-color-contrast-more); - border-radius: 6px; - outline: none; - - color: var(--font-color-inputs); - font-family: var(--font-secondary); - font-size: 11px; - line-height: 1em; - text-transform: uppercase; + position: sticky; + top: 0; + + flex: auto 0 0; + + box-sizing: border-box; + height: 36px; + padding: 10px 16px; + margin: 16px 16px 0 16px; + + background-color: var(--background-color-inputs); + border: 1px solid var(--background-color-contrast-more); + border-radius: 6px; + outline: none; + + color: var(--font-color-inputs); + font-family: var(--font-secondary); + font-size: 11px; + line-height: 1em; + text-transform: uppercase; } div.select > div.selector-tabs { - position: relative; - - flex: auto 0 0; - - display: flex; - padding: 16px; - border-bottom: 1px solid var(--background-color-contrast); + position: relative; + + flex: auto 0 0; + + display: flex; + padding: 16px; + border-bottom: 1px solid var(--background-color-contrast); } div.select > div.selector-tabs button { - box-sizing: border-box; - flex: 100% 1 1; - padding: 11px 0; - margin: 0; - - background-color: var(--background-color); - border: 1px solid var(--background-color-contrast-more); - - color: var(--accent-color-contrast); - font-family: var(--font-primary); - font-weight: 400; - font-size: 12px; - line-height: 1em; - - cursor: pointer; - outline: none; + box-sizing: border-box; + flex: 100% 1 1; + padding: 11px 0; + margin: 0; + + background-color: var(--background-color); + border: 1px solid var(--background-color-contrast-more); + + color: var(--accent-color-contrast); + font-family: var(--font-primary); + font-weight: 400; + font-size: 12px; + line-height: 1em; + + cursor: pointer; + outline: none; } div.select > div.selector-tabs button:nth-of-type(1) { - border-right: none; - border-radius: 6px 0px 0px 6px; + border-right: none; + border-radius: 6px 0px 0px 6px; } div.select > div.selector-tabs button:nth-last-of-type(1) { - border-left: none; - border-radius: 0 6px 6px 0; + border-left: none; + border-radius: 0 6px 6px 0; } svg.chevron, svg.stop { - display: none; + display: none; } div.select > div.selector-panel > div.scroll-container { - position: relative; - flex: auto 1 1; + position: relative; + flex: auto 1 1; } div.select > div.extra-panel { - padding: 16px 16px 0 16px; + padding: 16px 16px 0 16px; } div.scrollOuter { - } div.scrollOuter { - position: absolute; - top: 0; - z-index: 1; - - box-sizing: border-box; - width: calc(100% - 60px); - height: 100%; - - background-color: var(--background-color); - border-width: 0px; - border-style: solid; - border-color: var(--background-color-contrast-more); - - overflow-y: hidden; - overflow-x: hidden; - transform: none; - transition: transform 0.3s ease-out, right 0.3s ease-out; + position: absolute; + top: 0; + z-index: 1; + + box-sizing: border-box; + width: calc(100% - 60px); + height: 100%; + + background-color: var(--background-color); + border-width: 0px; + border-style: solid; + border-color: var(--background-color-contrast-more); + + overflow-y: hidden; + overflow-x: hidden; + transform: none; + transition: + transform 0.3s ease-out, + right 0.3s ease-out; } div.scrollOuter:before { - position: absolute; - top: 0; - z-index: 1; - - content: ''; - - box-sizing: border-box; - display: block; - width: 100%; - height: 100%; - - background-color: var(--background-color); - - opacity: 0.0; - transition: opacity 0.1s ease-out; - - pointer-events: none; + position: absolute; + top: 0; + z-index: 1; + + content: ""; + + box-sizing: border-box; + display: block; + width: 100%; + height: 100%; + + background-color: var(--background-color); + + opacity: 0; + transition: opacity 0.1s ease-out; + + pointer-events: none; } div.scrollOuter[data-list="brands"] { - left: 0; - padding: 0 16px; - - border-right: 1px solid inherit; + left: 0; + padding: 0 16px; + + border-right: 1px solid inherit; } div.scrollOuter[data-list="models"] { - right: 0; - - padding: 0 10px 0 16px; - - border-left-width: 1px; + right: 0; + + padding: 0 10px 0 16px; + + border-left-width: 1px; } div.scroll { - box-sizing: border-box; - width: 100%; - height: 100%; - padding: 16px 0; - - overflow-y: auto; - overflow-x: hidden; - - scroll-behavior: smooth; + box-sizing: border-box; + width: 100%; + height: 100%; + padding: 16px 0; + + overflow-y: auto; + overflow-x: hidden; + + scroll-behavior: smooth; } /* List selected styles */ /* Brands selected */ div.select[data-selected="brands"] > div.selector-tabs button.brands { - background-color: var(--accent-color); - border-color: var(--accent-color); - - color: var(--font-color-secondary); + background-color: var(--accent-color); + border-color: var(--accent-color); + + color: var(--font-color-secondary); } div.select[data-selected="brands"] div.scrollOuter[data-list="models"] { - transform: translateX(calc(100% - 60px)); + transform: translateX(calc(100% - 60px)); } div.select[data-selected="brands"] div.scrollOuter[data-list="models"]:before { - opacity: 0.8; - pointer-events: all; - cursor: pointer; + opacity: 0.8; + pointer-events: all; + cursor: pointer; } /* Models selected */ div.select[data-selected="models"] > div.selector-tabs button.models { - background-color: var(--accent-color); - border-color: var(--accent-color); - - color: var(--font-color-secondary); + background-color: var(--accent-color); + border-color: var(--accent-color); + + color: var(--font-color-secondary); } div.select[data-selected="models"] div.scrollOuter[data-list="brands"]:before { - opacity: 0.8; - pointer-events: all; - cursor: pointer; + opacity: 0.8; + pointer-events: all; + cursor: pointer; } div.select[data-selected="extra"] > div.selector-tabs button.extra { - background-color: var(--accent-color); - border-color: var(--accent-color); - - color: var(--font-color-secondary); + background-color: var(--accent-color); + border-color: var(--accent-color); + + color: var(--font-color-secondary); } /* List item */ div.scroll > div { - position: relative; - - box-sizing: border-box; - min-height: 36px; - padding: 11px 12px; - overflow: hidden; - - background-color: var(--background-color) !important; - border: none; - border-radius: 6px; - - color: var(--font-color-primary); - font-weight: 400; - font-size: 12px; - line-height: 1.5em; - - cursor: pointer; + position: relative; + + box-sizing: border-box; + min-height: 36px; + padding: 11px 12px; + overflow: hidden; + + background-color: var(--background-color) !important; + border: none; + border-radius: 6px; + + color: var(--font-color-primary); + font-weight: 400; + font-size: 12px; + line-height: 1.5em; + + cursor: pointer; } div.scroll > div + div { - margin-top: 6px; + margin-top: 6px; } div.scroll > div:hover { - text-decoration: underline; + text-decoration: underline; } div.scroll div.active, div.scroll div[style*="border"] { - background-color: var(--accent-color) !important; - - color: var(--font-color-secondary); + background-color: var(--accent-color) !important; + + color: var(--font-color-secondary); } div.scroll div.active:before { - position: absolute; - - top: 0; - right: 0; - - content: ''; - box-sizing: border-box; - display: block; - width: 36px; - height: 36px; - - border-radius: 6px; - - pointer-events: none; + position: absolute; + + top: 0; + right: 0; + + content: ""; + box-sizing: border-box; + display: block; + width: 36px; + height: 36px; + + border-radius: 6px; + + pointer-events: none; } div.scroll div.active:after { - position: absolute; - - top: 0; - right: 0; - - content: ''; - box-sizing: border-box; - display: block; - width: 36px; - height: 36px; - - background-color: var(--font-color-secondary); - - pointer-events: none; + position: absolute; + + top: 0; + right: 0; + + content: ""; + box-sizing: border-box; + display: block; + width: 36px; + height: 36px; + + background-color: var(--font-color-secondary); + + pointer-events: none; } div.scroll div.active:after { - mask: var(--icon-remove); - -webkit-mask: var(--icon-remove); - - mask-size: 16px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 16px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; + mask: var(--icon-remove); + -webkit-mask: var(--icon-remove); + + mask-size: 16px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 16px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; } div.scroll > div.phone-item { - display: flex; - justify-content: flex-start; - align-items: center; - - width: calc(350px - 94px); - padding: 0; - - border-right: none; + display: flex; + justify-content: flex-start; + align-items: center; + + width: calc(350px - 94px); + padding: 0; + + border-right: none; } div.scroll > div.phone-item span { - padding: 11px 12px; + padding: 11px 12px; } /* Plus button */ div.scroll > div.phone-item div.phone-item-add { - position: relative; - - box-sizing: border-box; - flex: 36px 0 0; - height: 36px; - margin: 0 0px 0 auto; - overflow: hidden; - - background-color: var(--background-color) !important; - border-radius: 6px; + position: relative; + + box-sizing: border-box; + flex: 36px 0 0; + height: 36px; + margin: 0 0px 0 auto; + overflow: hidden; + + background-color: var(--background-color) !important; + border-radius: 6px; } div.scroll > div.phone-item div.phone-item-add:before { - position: absolute; - top: 0; - left: 0; - - content: ''; - - box-sizing: border-box; - display: block; - width: 36px; - height: 36px; - background-color: var(--background-color-contrast-more); - - mask: var(--icon-plus); - -webkit-mask: var(--icon-plus); - mask-size: 16px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 16px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; - - pointer-events: none; + position: absolute; + top: 0; + left: 0; + + content: ""; + + box-sizing: border-box; + display: block; + width: 36px; + height: 36px; + background-color: var(--background-color-contrast-more); + + mask: var(--icon-plus); + -webkit-mask: var(--icon-plus); + mask-size: 16px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 16px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + + pointer-events: none; } div.scroll div[style*="border"] div.phone-item-add { - border-color: transparent !important; + border-color: transparent !important; } /* Remove button */ /* Currently hidden because of a bug that adds multiple remove buttons; want to unhide when fixed */ div.scroll > div.phone-item > span.remove { - position: relative; - - display: none; - box-sizing: border-box; - flex: 44px 0 0; - height: 38px; - margin: 0 3px 0 auto; - - background-color: var(--background-color-contrast) !important; - border-radius: 4px; - - font-size: 0px; + position: relative; + + display: none; + box-sizing: border-box; + flex: 44px 0 0; + height: 38px; + margin: 0 3px 0 auto; + + background-color: var(--background-color-contrast) !important; + border-radius: 4px; + + font-size: 0px; } div.scroll > div.phone-item > span.remove:before { - position: absolute; - top: 6px; - left: 3px; - - content: ''; - - box-sizing: border-box; - display: block; - width: calc(100% - 6px); - height: calc(100% - 12px); - background-color: var(--background-color-contrast-more); - - mask: var(--icon-remove); - -webkit-mask: var(--icon-remove); - mask-size: 16px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 16px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; - - pointer-events: none; + position: absolute; + top: 6px; + left: 3px; + + content: ""; + + box-sizing: border-box; + display: block; + width: calc(100% - 6px); + height: calc(100% - 12px); + background-color: var(--background-color-contrast-more); + + mask: var(--icon-remove); + -webkit-mask: var(--icon-remove); + mask-size: 16px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 16px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + + pointer-events: none; } /* Hack to display remove button that appears inside the add button */ div.scroll > div.phone-item[style*="border"] div.phone-item-add { - display: inherit; - - background-color: transparent !important; - pointer-events: none; + display: inherit; + + background-color: transparent !important; + pointer-events: none; } div.scroll div[style*="border"] div.phone-item-add:before { - display: none; - pointer-events: none; + display: none; + pointer-events: none; } div.scroll div[style*="border"] div.phone-item-add span.remove { - box-sizing: border-box; - flex: 36px 0 0; - height: 36px; - - border-radius: 6px; - - font-size: 0px; - pointer-events: all; + box-sizing: border-box; + flex: 36px 0 0; + height: 36px; + + border-radius: 6px; + + font-size: 0px; + pointer-events: all; } div.scroll div[style*="border"] div.phone-item-add span.remove:before { - position: absolute; - top: 0px; - left: 0px; - - content: ''; - - box-sizing: border-box; - display: block; - width: 100%; - height: 100%; - background-color: var(--background-color-contrast-more); - background-color: var(--font-color-secondary); - - mask: var(--icon-remove); - -webkit-mask: var(--icon-remove); - mask-size: 16px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 16px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; - - pointer-events: none; - - animation-name: remove-spin; - animation-duration: 0.2s; - animation-timing-function: ease-in-out; - animation-fill-mode: forwards; + position: absolute; + top: 0px; + left: 0px; + + content: ""; + + box-sizing: border-box; + display: block; + width: 100%; + height: 100%; + background-color: var(--background-color-contrast-more); + background-color: var(--font-color-secondary); + + mask: var(--icon-remove); + -webkit-mask: var(--icon-remove); + mask-size: 16px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 16px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + + pointer-events: none; + + animation-name: remove-spin; + animation-duration: 0.2s; + animation-timing-function: ease-in-out; + animation-fill-mode: forwards; } @keyframes remove-spin { - 0% { - transform: rotate(115deg); - } - 100% { - transform: rotate(0deg); - } + 0% { + transform: rotate(115deg); + } + 100% { + transform: rotate(0deg); + } } - - /***** Tutorial *****/ div.tutorial-overlay { - position: absolute; - top: 0; - left: 0; - z-index: 1; - - display: flex; - justify-content: flex-start; - align-items: center; - overflow: hidden; - - box-sizing: border-box; - width: 100%; - height: 100%; - padding: 1% 1.8% 3.2% 1.9%; - - pointer-events: none; + position: absolute; + top: 0; + left: 0; + z-index: 1; + + display: flex; + justify-content: flex-start; + align-items: center; + overflow: hidden; + + box-sizing: border-box; + width: 100%; + height: 100%; + padding: 1% 1.8% 3.2% 1.9%; + + pointer-events: none; } div.overlay-segment { - box-sizing: border-box; - flex-grow: 0; - flex-shrink: 0; - height: 100%; - - background-color: var(--accent-color); - - opacity: 0.0; - - transition: opacity 0.1s ease; + box-sizing: border-box; + flex-grow: 0; + flex-shrink: 0; + height: 100%; + + background-color: var(--accent-color); + + opacity: 0; + + transition: opacity 0.1s ease; } div.tutorial-buttons { - position: relative; - - display: flex; - align-items: flex-start; - - box-sizing: border-box; - width: 100%; - height: 45px; - padding: 6px 0 0 16px; - - background-color: var(--background-color-graph); - - overflow-x: auto; + position: relative; + + display: flex; + align-items: flex-start; + + box-sizing: border-box; + width: 100%; + height: 45px; + padding: 6px 0 0 16px; + + background-color: var(--background-color-graph); + + overflow-x: auto; } div.tutorial-buttons:after { - content: ''; - display: inline-block; - flex: 1px 0 0; - height: 100%; - background-color: transparent; + content: ""; + display: inline-block; + flex: 1px 0 0; + height: 100%; + background-color: transparent; } button.button-segment { - position: relative; - - flex: auto 1 0; - padding: 10px 16px; - margin: 0; - - background-color: var(--background-color); - background-color: transparent; - border: 0; - outline: none; - - color: var(--font-color-primary); - font-family: var(--font-primary); - font-weight: 400; - font-size: 12px; - line-height: 1em; - - cursor: pointer; + position: relative; + + flex: auto 1 0; + padding: 10px 16px; + margin: 0; + + background-color: var(--background-color); + background-color: transparent; + border: 0; + outline: none; + + color: var(--font-color-primary); + font-family: var(--font-primary); + font-weight: 400; + font-size: 12px; + line-height: 1em; + + cursor: pointer; } button.button-segment:last-child { - margin-right: 15px; + margin-right: 15px; } div.tutorial-description { - display: flex; - - max-height: 0px; - overflow: hidden; - - background-color: var(--background-color-graph); - border-bottom: 1px solid var(--background-color-contrast-more); - - transition: max-height 0.2s ease-in-out; + display: flex; + + max-height: 0px; + overflow: hidden; + + background-color: var(--background-color-graph); + border-bottom: 1px solid var(--background-color-contrast-more); + + transition: max-height 0.2s ease-in-out; } section.parts-primary[tutorial-active="true"] div.tutorial-description { - max-height: 500px; + max-height: 500px; } article.description-segment { - display: flex; - align-items: center; - justify-content: center; - - order: 1; - flex: auto 0 0; - width: 100%; + display: flex; + align-items: center; + justify-content: center; + + order: 1; + flex: auto 0 0; + width: 100%; } article.description-segment p { - padding: 16px; - margin: 0 16px 16px 16px; - - flex: auto 1 1; - max-width: 800px; - - color: var(--font-color-primary); - font-family: var(--font-primary); - font-weight: 400; - font-size: 14px; - line-height: 1.6em; - text-align: center; + padding: 16px; + margin: 0 16px 16px 16px; + + flex: auto 1 1; + max-width: 800px; + + color: var(--font-color-primary); + font-family: var(--font-primary); + font-weight: 400; + font-size: 14px; + line-height: 1.6em; + text-align: center; } div.overlay-segment[tutorial-hover="true"] { - opacity: 0.05; + opacity: 0.05; } section.parts-primary[tutorial-active="true"] div.overlay-segment[tutorial-on="false"] { - opacity: 0.0; + opacity: 0; } div.overlay-segment[tutorial-on="true"] { - opacity: 0.1; + opacity: 0.1; } button.button-segment[tutorial-on="true"] { } button.button-segment[tutorial-on="true"]:before { - position: absolute; - top: 0; - left: 0; - - content: ''; - display: block; - width: 100%; - height: 100%; - - background-color: var(--accent-color); - border-radius: 100px; - - opacity: 0.1; + position: absolute; + top: 0; + left: 0; + + content: ""; + display: block; + width: 100%; + height: 100%; + + background-color: var(--accent-color); + border-radius: 100px; + + opacity: 0.1; } article.description-segment[tutorial-on="true"] { - order: 0; + order: 0; } - - /***** External links *****/ div.external-links { - display: flex; - align-items: flex-start; - - box-sizing: border-box; - width: 100%; - max-height: 69px; - padding: 16px; - overflow-x: auto; - overflow-y: hidden; - - border-top: 1px solid var(--background-color-contrast-more); + display: flex; + align-items: flex-start; + + box-sizing: border-box; + width: 100%; + max-height: 69px; + padding: 16px; + overflow-x: auto; + overflow-y: hidden; + + border-top: 1px solid var(--background-color-contrast-more); } div.external-links:empty { - display: none; + display: none; } div.external-links:after { - content: ''; - display: inline-block; - flex: 16px 0 0; - height: 1px; + content: ""; + display: inline-block; + flex: 16px 0 0; + height: 1px; } div.external-links span { - flex: auto 0 0; - margin: 0 10px 0 0; - - color: var(--background-color-contrast-more); - font-family: var(--font-secondary); - font-size: 11px; - line-height: 1em; - text-transform: uppercase; - line-height: 36px; - - white-space: nowrap; + flex: auto 0 0; + margin: 0 10px 0 0; + + color: var(--background-color-contrast-more); + font-family: var(--font-secondary); + font-size: 11px; + line-height: 1em; + text-transform: uppercase; + line-height: 36px; + + white-space: nowrap; } div.external-links a + span { - padding-left: 16px; - margin-left: 16px; - border-left: 1px dotted var(--background-color-contrast-more); + padding-left: 16px; + margin-left: 16px; + border-left: 1px dotted var(--background-color-contrast-more); } div.external-links a { - flex: auto 0 0; - - box-sizing: border-box; - min-width: 100px; - padding: 11px 16px; - - background-color: var(--background-color); - border: 1px solid var(--background-color-contrast); - border-radius: 6px; - - color: var(--background-color-contrast-more); - font-weight: 400; - font-size: 12px; - line-height: 1em; - text-align: center; - text-decoration: none; - - white-space: nowrap; - cursor: pointer; - outline: none; + flex: auto 0 0; + + box-sizing: border-box; + min-width: 100px; + padding: 11px 16px; + + background-color: var(--background-color); + border: 1px solid var(--background-color-contrast); + border-radius: 6px; + + color: var(--background-color-contrast-more); + font-weight: 400; + font-size: 12px; + line-height: 1em; + text-align: center; + text-decoration: none; + + white-space: nowrap; + cursor: pointer; + outline: none; } -div.external-links a.active { - color: var(--background-color-contrast); - background-color: var(--background-color-contrast-more); - border-color: var(--background-color-contrast-more); +div.external-links a.active { + color: var(--background-color-contrast); + background-color: var(--background-color-contrast-more); + border-color: var(--background-color-contrast-more); } div.external-links a + a { - margin-left: 6px; + margin-left: 6px; } - - /***** Accessories *****/ div.accessories { - box-sizing: border-box; - padding: 16px 0; - - border-top: 1px solid var(--background-color-contrast-more); - background-color: var(--background-color); + box-sizing: border-box; + padding: 16px 0; + + border-top: 1px solid var(--background-color-contrast-more); + background-color: var(--background-color); } div.accessories:empty { - display: none; + display: none; } div.accessories h2 { - width: calc(100% - 32px); - max-width: 660px; - margin: 32px auto 16px auto; - - border-radius: 4px; - - color: var(--background-color-contrast-more); - font-family: var(--font-secondary); - font-weight: 400; - font-size: 14px; - line-height: 1.6em; - text-transform: uppercase; + width: calc(100% - 32px); + max-width: 660px; + margin: 32px auto 16px auto; + + border-radius: 4px; + + color: var(--background-color-contrast-more); + font-family: var(--font-secondary); + font-weight: 400; + font-size: 14px; + line-height: 1.6em; + text-transform: uppercase; } div.accessories p { - width: calc(100% - 32px); - max-width: 660px; - margin: 16px auto; - - border-radius: 4px; - - color: var(--font-color-primary); - font-size: 14px; - line-height: 1.6em; + width: calc(100% - 32px); + max-width: 660px; + margin: 16px auto; + + border-radius: 4px; + + color: var(--font-color-primary); + font-size: 14px; + line-height: 1.6em; } div.accessories p.center { - max-width: 800px; - - color: var(--background-color-contrast-more); - text-align: center; + max-width: 800px; + + color: var(--background-color-contrast-more); + text-align: center; } div.accessories a, div.accessories a:hover, div.accessories a:visited { - color: var(--background-color-contrast-more); + color: var(--background-color-contrast-more); } div.accessories-widgets { - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: center; - - padding: 0 16px 16px 0; -} + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; -div.accessories-widgets .widget { - flex: auto 0 0; - - margin: 16px 0 0 16px; + padding: 0 16px 16px 0; } +div.accessories-widgets .widget { + flex: auto 0 0; + margin: 16px 0 0 16px; +} /***** Restricted mode / Cash message style *****/ div.cashMessage { - position: absolute; - top: 0; - left: 0; - z-index: 3; - - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - width: 100%; - height: 100%; - - pointer-events: none; - + position: absolute; + top: 0; + left: 0; + z-index: 3; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + width: 100%; + height: 100%; + + pointer-events: none; } div.cashMessage h2 { - flex: auto 0 1; - - box-sizing: border-box; - max-width: 600px; - margin: 32px 32px 16px 32px;; - - font-weight: 700; - font-size: 18px; - line-height: 1em; - - text-align: center; - - pointer-events: all; + flex: auto 0 1; + + box-sizing: border-box; + max-width: 600px; + margin: 32px 32px 16px 32px; + + font-weight: 700; + font-size: 18px; + line-height: 1em; + + text-align: center; + + pointer-events: all; } div.cashMessage p { - flex: auto 0 1; - - box-sizing: border-box; - max-width: 600px; - margin: 16px 32px; - - font-weight: 400; - font-size: 16px; - line-height: 1.5em; - - text-align: center; - - pointer-events: all; + flex: auto 0 1; + + box-sizing: border-box; + max-width: 600px; + margin: 16px 32px; + + font-weight: 400; + font-size: 16px; + line-height: 1.5em; + + text-align: center; + + pointer-events: all; } div.cashMessage p:empty { - display: none; + display: none; } div.cashMessage p a { - font-weight: 700; - color: var(--accent-color); + font-weight: 700; + color: var(--accent-color); } div.cashMessage button { - flex: auto 0 1; - - box-sizing: border-box; - width: 200px; - padding: 12px 16px; - margin: 16px 32px 32px 32px; - - background-color: var(--accent-color-contrast); - border-radius: 50px; - border: none; - outline: none; - - color: var(--accent-color); - font-weight: 700; - font-size: 12px; - line-height: 1em; - text-align: center; + flex: auto 0 1; - - white-space: nowrap; - cursor: pointer; - pointer-events: all; - - animation-name: cash-fine; - animation-duration: 0.3s; - animation-delay: 1.0s; - animation-iteration-count: 1; - animation-timing-function: ease-in-out; - animation-fill-mode: forwards; - - opacity: 0.0; + box-sizing: border-box; + width: 200px; + padding: 12px 16px; + margin: 16px 32px 32px 32px; + + background-color: var(--accent-color-contrast); + border-radius: 50px; + border: none; + outline: none; + + color: var(--accent-color); + font-weight: 700; + font-size: 12px; + line-height: 1em; + text-align: center; + + white-space: nowrap; + cursor: pointer; + pointer-events: all; + + animation-name: cash-fine; + animation-duration: 0.3s; + animation-delay: 1s; + animation-iteration-count: 1; + animation-timing-function: ease-in-out; + animation-fill-mode: forwards; + + opacity: 0; } @keyframes cash-fine { - 0% { - opacity: 0.0; - transform: translateY(10px); - } - - 100% { - opacity: 1.0; - transform: translateY(0px); - } + 0% { + opacity: 0; + transform: translateY(10px); + } + + 100% { + opacity: 1; + transform: translateY(0px); + } } div.cashMessage button:active { - background-color: var(--accent-color); - color: var(--font-color-secondary); + background-color: var(--accent-color); + color: var(--font-color-secondary); } div.fadeAll { - position: absolute; - top: 0; - left: 0; - z-index: 2; - - width: 100%; - height: 100%; - - background-color: var(--background-color); - - opacity: 0.9; - - cursor: pointer; - - animation-name: cash-in; - animation-duration: 0.1s; - animation-iteration-count: 1; - animation-timing-function: ease-in-out; - animation-fill-mode: forwards; + position: absolute; + top: 0; + left: 0; + z-index: 2; + + width: 100%; + height: 100%; + + background-color: var(--background-color); + + opacity: 0.9; + + cursor: pointer; + + animation-name: cash-in; + animation-duration: 0.1s; + animation-iteration-count: 1; + animation-timing-function: ease-in-out; + animation-fill-mode: forwards; } @keyframes cash-in { - 0% { - opacity: 0.0; - } - - 100% { - opacity: 0.97; - } + 0% { + opacity: 0; + } + + 100% { + opacity: 0.97; + } } div.fadeAll:before { - position: absolute; - top: 12px; - right: 22px; - - content: ''; - - box-sizing: border-box; - display: block; - width: 44px; - height: 38px; - - background-color: var(--background-color-contrast); - border-radius: 4px; - - pointer-events: none; + position: absolute; + top: 12px; + right: 22px; + + content: ""; + + box-sizing: border-box; + display: block; + width: 44px; + height: 38px; + + background-color: var(--background-color-contrast); + border-radius: 4px; + + pointer-events: none; } div.fadeAll:after { - position: absolute; - top: 12px; - right: 22px; - - content: ''; - - box-sizing: border-box; - display: block; - width: 44px; - height: 38px; - - background-color: var(--background-color-contrast-more); - - mask: var(--icon-remove); - -webkit-mask: var(--icon-remove); - mask-size: 20px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 20px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; - - pointer-events: none; + position: absolute; + top: 12px; + right: 22px; + + content: ""; + + box-sizing: border-box; + display: block; + width: 44px; + height: 38px; + + background-color: var(--background-color-contrast-more); + + mask: var(--icon-remove); + -webkit-mask: var(--icon-remove); + mask-size: 20px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 20px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + + pointer-events: none; } div.cash { + position: absolute; + top: 0; + left: 0; + z-index: 1; + + display: flex; + align-items: center; + justify-content: center; + + width: 100%; + height: 100%; + + font-family: var(--font-secondary); + font-size: 30vh; + font-weight: 700; + line-height: 1em; + + cursor: pointer; +} + +/***** +Responsive styles *****/ + +@media (max-width: 1000px) { + ::-webkit-scrollbar { + width: 0px; + } + + body { + overflow: hidden; + } + + main.main { + position: relative; + + display: block; + overflow: hidden; + } + + section.parts-primary { + position: relative; + + order: 1; + height: 100%; + overflow-y: auto; + } + + section.parts-primary:after { + content: ""; + + display: block; + height: 84px; + } + + div.graphBox { + top: 0; + z-index: 2; + padding-bottom: 8px; + } + + div.graph-sizer { + margin: 8px auto 0 auto; + } + + div.targets { + } + + div.manage { + position: relative; + background-color: var(--background-color-contrast); + } + + div.manage:before { position: absolute; top: 0; left: 0; z-index: 1; - - display: flex; - align-items: center; - justify-content: center; - + + content: ""; + display: block; width: 100%; height: 100%; - - font-family: var(--font-secondary); - font-size: 30vh; - font-weight: 700; - line-height: 1em; - - cursor: pointer; -} + background-color: var(--background-color-contrast-more); + pointer-events: none; + opacity: 0; + transition: opacity 0.2s ease-in 0.2s; + } -/***** -Responsive styles *****/ + table.manageTable { + min-height: calc(100vh - ((100vw * 0.44) + 92px)); + } -@media ( max-width: 1000px ) { - ::-webkit-scrollbar { - width: 0px; - } - - body { - overflow: hidden; - } - - - main.main { - position: relative; - - display: block; - overflow: hidden; - } - - section.parts-primary { - position: relative; - - order: 1; - height: 100%; - overflow-y: auto; - } - - section.parts-primary:after { - content: ''; - - display: block; - height: 84px; - } - - div.graphBox { - top: 0; - z-index: 2; - padding-bottom: 8px; - } - - div.graph-sizer { - margin: 8px auto 0 auto; - } - - div.targets { - - } - - div.manage { - position: relative; - background-color: var(--background-color-contrast); - } - - div.manage:before { - position: absolute; - top: 0; - left: 0; - z-index: 1; - - content: ''; - display: block; - width: 100%; - height: 100%; - - background-color: var(--background-color-contrast-more); - pointer-events: none; - - opacity: 0.0; - transition: opacity 0.2s ease-in 0.2s; - } - - table.manageTable { - min-height: calc( 100vh - ((100vw * 0.44) + 92px) ); - } - - tbody.curves > tr { - border-top: 1px solid var(--background-color); - } -/* + tbody.curves > tr { + border-top: 1px solid var(--background-color); + } + /* tbody.curves > tr > td.button-baseline:before, tbody.curves > tr > td.hideIcon:before, @@ -3039,469 +3118,424 @@ Responsive styles *****/ background-color: var(--background-color-contrast); } */ - - tbody.curves > tr > td.hideIcon.selected:before, - tbody.curves > tr > td.button-pin:before, - tbody.curves > tr > td.button-export:before, - tbody.curves > tr > td.remove:before { - border: 1px solid var(--background-color); - background-color: var(--background-color-contrast); - } - - tbody.curves > tr > td.button-baseline:before, - tbody.curves > tr > td.hideIcon:before, - tbody.curves > tr > td.button-pin[data-pinned="true"]:before { - background-color: var(--background-color-contrast); - } - - section.parts-secondary { - position: absolute; - top: 0; - z-index: 1; - - order: 2; - width: 100%; - height: calc(100% - ((100vw * 0.44) + 92px)); - min-height: 200px; - margin-top: 0px; - - border-right: none; - border-top: 1px solid var(--background-color-contrast-more); - border-radius: 16px 16px 0 0; - - transition: all 200ms ease-in; - transform: translateY(calc( (100vw * 0.44) + 92px )); - } - - div.controls { - border-radius: 16px 16px 0 0; - } - - div.controls:before { - position: absolute; - top: 6px; - left: 0; - right: 0; - - content: ''; - display: block; - margin: auto; - - width: 60px; - height: 4px; - - background-color: var(--background-color-contrast-more); - border-radius: 6px; - - pointer-events: none; - } - - div.scroll > div.phone-item { - width: calc(100vw - 94px); - } - - tbody.curves:empty:before { - content: 'Select a model from the list below to graph its frequency response'; - } - - tr.mobile-helper { - position: relative; - - display: block; - padding: 16px; - } - - tr.mobile-helper:before { - z-index: 1; - - content: 'Browse all graphs'; - display: block; - - box-sizing: border-box; - width: 100%; - height: 36px; - padding: 11px 16px; - - background-color: var(--background-color); - border: 1px solid var(--background-color-contrast-more); - border-radius: 6px; - - color: var(--font-color-primary); - font-size: 12px; - line-height: 1em; - text-align: center; - - cursor: pointer; - } - - tr.mobile-helper:after { - position: absolute; - top: 0; - left: 0; - z-index: 1; - - content: ''; - display: block; - - box-sizing: border-box; - width: 100%; - height: 36px; - margin-top: 16px; - - background-color: var(--background-color-contrast-more); - - mask: var(--icon-plus); - -webkit-mask: var(--icon-plus); - mask-size: 16px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 16px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: right 26px center; - - cursor: pointer; - } - - /* Primary panel focused */ - main.main[data-focused-panel="primary"] div.manage:before { - opacity: 0.0; - } - - main.main[data-focused-panel="primary"] section.parts-secondary { - transform: translateY(calc( ((100vw * 0.44) + 92px) + 100% - 84px )); - - animation-name: graph-drawer-hide; - animation-duration: 0.4s; - animation-timing-function: ease-in; - animation-iteration-count: 1; - animation-fill-mode: forwards; - } - - @keyframes graph-drawer-hide { - 0% { - transform: translateY(calc( (100vw * 0.44) + 92px )); - } - 75% { - transform: translateY(calc( ((100vw * 0.44) + 92px) + 100% - 84px )); - } - 85% { - transform: translateY(calc( ((100vw * 0.44) + 92px) + 100% - 74px )); - } - 100% { - transform: translateY(calc( ((100vw * 0.44) + 92px) + 100% - 84px )); - } - } - - main.main[data-focused-panel="primary"] section.parts-secondary div.select:before { - animation-name: all-graphs-bounce; - animation-duration: 0.3s; - animation-iteration-count: 1; - animation-delay: 0.3s; - animation-timing-function: ease-out; - animation-fill-mode: forwards; - } - - @keyframes all-graphs-bounce { - 0% { - margin: -36px 16px 0 16px; - } - 100% { - margin: 16px 16px 32px 16px; - } - } - - /* Secondary panel focused */ - main.main[data-focused-panel="secondary"] section.parts-primary { - overflow: hidden; - } - - main.main[data-focused-panel="secondary"] div.manage:before { - opacity: 0.8; - transition: opacity 0.2s ease-in 0.0s; - } - - main.main[data-focused-panel="secondary"] section.parts-secondary { - animation-name: graph-drawer-show; - animation-duration: 0.4s; - animation-timing-function: ease-in; - animation-iteration-count: 1; - animation-fill-mode: forwards; - } - - @keyframes graph-drawer-show { - 0% { - transform: translateY(calc( ((100vw * 0.44) + 92px) + 100% - 84px )); - } - 75% { - transform: translateY(calc( (100vw * 0.44) + 92px )); - } - 85% { - transform: translateY(calc( (100vw * 0.44) + 102px )); - } - 100% { - height: calc(100% - ((100vw * 0.44) + 92px)); - min-height: 200px; - transform: translateY(calc( (100vw * 0.44) + 92px )); - } - } - /***** - Alt header mobile *****/ + tbody.curves > tr > td.hideIcon.selected:before, + tbody.curves > tr > td.button-pin:before, + tbody.curves > tr > td.button-export:before, + tbody.curves > tr > td.remove:before { + border: 1px solid var(--background-color); + background-color: var(--background-color-contrast); + } - button.header-button { - box-sizing: border-box; - display: block; - flex: 36px 0 0; - height: 36px; - margin: 0 0 0 6px; - - background-color: var(--header-menu-icon-color); - mask: var(--icon-hamburger); - -webkit-mask: var(--icon-hamburger); - mask-size: 20px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 20px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; - - cursor: pointer; - } + tbody.curves > tr > td.button-baseline:before, + tbody.curves > tr > td.hideIcon:before, + tbody.curves > tr > td.button-pin[data-pinned="true"]:before { + background-color: var(--background-color-contrast); + } - div.logo { - flex: auto; - margin: 0 42px 0 0; - } - - div.logo a { - width: 80%; - } + section.parts-secondary { + position: absolute; + top: 0; + z-index: 1; - ul.header-links { - position: fixed; - top: 49px; - left: 0; - z-index: 2; + order: 2; + width: 100%; + height: calc(100% - ((100vw * 0.44) + 92px)); + min-height: 200px; + margin-top: 0px; - width: 100%; - flex-direction: column; - justify-content: flex-start; - align-items: center; + border-right: none; + border-top: 1px solid var(--background-color-contrast-more); + border-radius: 16px 16px 0 0; - box-sizing: border-box; - height: calc(100vh - 49px); - max-height: -webkit-fill-available; - padding: 0 16px 16px 16px; + transition: all 200ms ease-in; + transform: translateY(calc((100vw * 0.44) + 92px)); + } - background-color: var(--background-color); + div.controls { + border-radius: 16px 16px 0 0; + } - transform: translateX(-100vw); - - overflow-x: hidden; - overflow-y: auto; - } + div.controls:before { + position: absolute; + top: 6px; + left: 0; + right: 0; - ul.header-links li { - text-align: center; - margin: 16px 0 0 0; - } + content: ""; + display: block; + margin: auto; - ul.header-links li + li { - margin: 32px 0 0 0; - } + width: 60px; + height: 4px; - ul.header-links li a { - display: block; - padding: 16px; - color: var(--font-color-primary); - } + background-color: var(--background-color-contrast-more); + border-radius: 6px; - header.header[data-links="collapsed"] ul.header-links { - transform: translateX(-100vw); - transition: transform 0.2s ease; - } + pointer-events: none; + } - header.header[data-links="expanded"] ul.header-links { - transform: translateX(0vw); - transition: transform 0.2s ease; - } -} + div.scroll > div.phone-item { + width: calc(100vw - 94px); + } -/* Move table items to two rows for narrow browsers */ -@media ( max-width: 1200px ) { - tbody.curves > tr { - flex-wrap: wrap; - } - - tbody.curves > tr > td.curve-color { - order: 2 !important; - } - - tbody.curves > tr > td.item-line { - flex: 100% 0 0 !important; - padding: 0 16px !important; - -/* border-bottom: 1px solid var(--background-color-contrast);*/ - } - - tbody.curves > tr > td.item-line div.phonename { - padding-right: 0px !important; - } - - tbody.curves > tr > td.channels { - margin-right: auto; - } - - tbody.curves > tr > td.levels { - margin-left: auto; - } - - tbody.curves > tr > td.button-baseline { - } - - tbody.curves > tr > td.remove { - } -} + tbody.curves:empty:before { + content: "Select a model from the list below to graph its frequency response"; + } -/* Move phones list to two columns for wide browsers */ -@media ( min-width: 1500px) and ( min-aspect-ratio: 2/1 ) { - section.parts-secondary { - flex: 600px 0 0; - } - - div.select > div.selector-panel > div.scroll-container { - display: flex; - max-height: 100%; - } - - div.scrollOuter { - position: relative; - - display: flex; - - flex: 50% 1 1; - height: calc(100% - 52px); + tr.mobile-helper { + position: relative; + + display: block; + padding: 16px; + } + + tr.mobile-helper:before { + z-index: 1; + + content: "Browse all graphs"; + display: block; + + box-sizing: border-box; + width: 100%; + height: 36px; + padding: 11px 16px; + + background-color: var(--background-color); + border: 1px solid var(--background-color-contrast-more); + border-radius: 6px; + + color: var(--font-color-primary); + font-size: 12px; + line-height: 1em; + text-align: center; + + cursor: pointer; + } + + tr.mobile-helper:after { + position: absolute; + top: 0; + left: 0; + z-index: 1; + + content: ""; + display: block; + + box-sizing: border-box; + width: 100%; + height: 36px; + margin-top: 16px; + + background-color: var(--background-color-contrast-more); + + mask: var(--icon-plus); + -webkit-mask: var(--icon-plus); + mask-size: 16px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 16px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: right 26px center; + + cursor: pointer; + } + + /* Primary panel focused */ + main.main[data-focused-panel="primary"] div.manage:before { + opacity: 0; + } + + main.main[data-focused-panel="primary"] section.parts-secondary { + transform: translateY(calc(((100vw * 0.44) + 92px) + 100% - 84px)); + + animation-name: graph-drawer-hide; + animation-duration: 0.4s; + animation-timing-function: ease-in; + animation-iteration-count: 1; + animation-fill-mode: forwards; + } + + @keyframes graph-drawer-hide { + 0% { + transform: translateY(calc((100vw * 0.44) + 92px)); } - - div.scroll { - position: relative; - - flex: 50% 1 1; + 75% { + transform: translateY(calc(((100vw * 0.44) + 92px) + 100% - 84px)); } - - div.scrollOuter[data-list="brands"]:before, - div.scrollOuter[data-list="models"]:before { - display: none; + 85% { + transform: translateY(calc(((100vw * 0.44) + 92px) + 100% - 74px)); } - - div[data-selected="brands"] div.scroll#phones { - transform: none; - transition: none; + 100% { + transform: translateY(calc(((100vw * 0.44) + 92px) + 100% - 84px)); } -} + } -/* Yeet table items for narrow screens */ -@media ( max-width: 500px) { - - tbody.curves > tr:before { - order: 5; - - content:''; - display: block; - flex: 100% 1 1; - - height: 0px; - } + main.main[data-focused-panel="primary"] section.parts-secondary div.select:before { + animation-name: all-graphs-bounce; + animation-duration: 0.3s; + animation-iteration-count: 1; + animation-delay: 0.3s; + animation-timing-function: ease-out; + animation-fill-mode: forwards; + } - tbody.curves > tr > td.item-line { + @keyframes all-graphs-bounce { + 0% { + margin: -36px 16px 0 16px; } - - tbody.curves > tr > td.curve-color { + 100% { + margin: 16px 16px 32px 16px; } - - tbody.curves > tr > td.channels { - order: 3; + } + + /* Secondary panel focused */ + main.main[data-focused-panel="secondary"] section.parts-primary { + overflow: hidden; + } + + main.main[data-focused-panel="secondary"] div.manage:before { + opacity: 0.8; + transition: opacity 0.2s ease-in 0s; + } + + main.main[data-focused-panel="secondary"] section.parts-secondary { + animation-name: graph-drawer-show; + animation-duration: 0.4s; + animation-timing-function: ease-in; + animation-iteration-count: 1; + animation-fill-mode: forwards; + } + + @keyframes graph-drawer-show { + 0% { + transform: translateY(calc(((100vw * 0.44) + 92px) + 100% - 84px)); } - - tbody.curves > tr > td.levels { - order: 4; - - margin-right: 16px; + 75% { + transform: translateY(calc((100vw * 0.44) + 92px)); } - - tbody.curves > tr > td.button-baseline { - margin-left: auto; + 85% { + transform: translateY(calc((100vw * 0.44) + 102px)); } - - tbody.curves > tr > td.button-baseline, - tbody.curves > tr > td.hideIcon, - tbody.curves > tr > td.button-pin { - order: 5; + 100% { + height: calc(100% - ((100vw * 0.44) + 92px)); + min-height: 200px; + transform: translateY(calc((100vw * 0.44) + 92px)); } + } + + /***** + Alt header mobile *****/ + + button.header-button { + box-sizing: border-box; + display: block; + flex: 36px 0 0; + height: 36px; + margin: 0 0 0 6px; + + background-color: var(--header-menu-icon-color); + mask: var(--icon-hamburger); + -webkit-mask: var(--icon-hamburger); + mask-size: 20px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 20px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + + cursor: pointer; + } + + div.logo { + flex: auto; + margin: 0 42px 0 0; + } + + div.logo a { + width: 80%; + } + + ul.header-links { + position: fixed; + top: 49px; + left: 0; + z-index: 2; + + width: 100%; + flex-direction: column; + justify-content: flex-start; + align-items: center; + + box-sizing: border-box; + height: calc(100vh - 49px); + max-height: -webkit-fill-available; + padding: 0 16px 16px 16px; + + background-color: var(--background-color); + + transform: translateX(-100vw); + + overflow-x: hidden; + overflow-y: auto; + } + + ul.header-links li { + text-align: center; + margin: 16px 0 0 0; + } + + ul.header-links li + li { + margin: 32px 0 0 0; + } + + ul.header-links li a { + display: block; + padding: 16px; + color: var(--font-color-primary); + } + + header.header[data-links="collapsed"] ul.header-links { + transform: translateX(-100vw); + transition: transform 0.2s ease; + } + + header.header[data-links="expanded"] ul.header-links { + transform: translateX(0vw); + transition: transform 0.2s ease; + } } -/*@media ( max-height: 500px ) and ( orientation: landscape ) {*/ -@media ( max-height: 500px ) and ( min-aspect-ratio: 3 / 2 ) { - header.header { - display: none; - } - - body:not([data-input-state="focus"]) section.parts-primary { - z-index: 3; - - flex: 100vh 1 1 !important; - max-height: -webkit-fill-available; - } - - body:not([data-input-state="focus"]) section.parts-secondary { - display: none; - } - - body:not([data-input-state="focus"]) div.graph-sizer { - position: fixed; - z-index: 5; - top: 0; - left: 0; - - display: flex; - justify-content: center; - align-items: center; - - box-sizing: border-box; - width: 100vw; - height: 100vh; - max-width: none; - max-height: -webkit-fill-available; - margin: 0; - - background-color: var(--background-color); - } - - body:not([data-input-state="focus"]) svg#fr-graph { - width: 100%; - - pointer-events: none; - } +/* Move table items to two rows for narrow browsers */ +@media (max-width: 1200px) { + tbody.curves > tr { + flex-wrap: wrap; + } + + tbody.curves > tr > td.curve-color { + order: 2 !important; + } + + tbody.curves > tr > td.item-line { + flex: 100% 0 0 !important; + padding: 0 16px !important; + + /* border-bottom: 1px solid var(--background-color-contrast);*/ + } + + tbody.curves > tr > td.item-line div.phonename { + padding-right: 0px !important; + } + + tbody.curves > tr > td.channels { + margin-right: auto; + } + + tbody.curves > tr > td.levels { + margin-left: auto; + } + + tbody.curves > tr > td.button-baseline { + } + + tbody.curves > tr > td.remove { + } } -/***** -Embed mode *****/ -body[embed-mode="true"] header.header { +/* Move phones list to two columns for wide browsers */ +@media (min-width: 1500px) and (min-aspect-ratio: 2/1) { + section.parts-secondary { + flex: 600px 0 0; + } + + div.select > div.selector-panel > div.scroll-container { + display: flex; + max-height: 100%; + } + + div.scrollOuter { + position: relative; + + display: flex; + + flex: 50% 1 1; + height: calc(100% - 52px); + } + + div.scroll { + position: relative; + + flex: 50% 1 1; + } + + div.scrollOuter[data-list="brands"]:before, + div.scrollOuter[data-list="models"]:before { display: none; + } + + div[data-selected="brands"] div.scroll#phones { + transform: none; + transition: none; + } } -body[embed-mode="true"] section.parts-primary { +/* Yeet table items for narrow screens */ +@media (max-width: 500px) { + tbody.curves > tr:before { + order: 5; + + content: ""; + display: block; + flex: 100% 1 1; + + height: 0px; + } + + tbody.curves > tr > td.item-line { + } + + tbody.curves > tr > td.curve-color { + } + + tbody.curves > tr > td.channels { + order: 3; + } + + tbody.curves > tr > td.levels { + order: 4; + + margin-right: 16px; + } + + tbody.curves > tr > td.button-baseline { + margin-left: auto; + } + + tbody.curves > tr > td.button-baseline, + tbody.curves > tr > td.hideIcon, + tbody.curves > tr > td.button-pin { + order: 5; + } +} + +/*@media ( max-height: 500px ) and ( orientation: landscape ) {*/ +@media (max-height: 500px) and (min-aspect-ratio: 3 / 2) { + header.header { + display: none; + } + + body:not([data-input-state="focus"]) section.parts-primary { z-index: 3; flex: 100vh 1 1 !important; max-height: -webkit-fill-available; -} + } -body[embed-mode="true"] section.parts-secondary { + body:not([data-input-state="focus"]) section.parts-secondary { display: none; -} + } -body[embed-mode="true"] div.graph-sizer { + body:not([data-input-state="focus"]) div.graph-sizer { position: fixed; z-index: 5; top: 0; @@ -3519,10 +3553,54 @@ body[embed-mode="true"] div.graph-sizer { margin: 0; background-color: var(--background-color); -} + } -body[embed-mode="true"] svg#fr-graph { + body:not([data-input-state="focus"]) svg#fr-graph { width: 100%; pointer-events: none; + } +} + +/***** +Embed mode *****/ +body[embed-mode="true"] header.header { + display: none; +} + +body[embed-mode="true"] section.parts-primary { + z-index: 3; + + flex: 100vh 1 1 !important; + max-height: -webkit-fill-available; +} + +body[embed-mode="true"] section.parts-secondary { + display: none; +} + +body[embed-mode="true"] div.graph-sizer { + position: fixed; + z-index: 5; + top: 0; + left: 0; + + display: flex; + justify-content: center; + align-items: center; + + box-sizing: border-box; + width: 100vw; + height: 100vh; + max-width: none; + max-height: -webkit-fill-available; + margin: 0; + + background-color: var(--background-color); +} + +body[embed-mode="true"] svg#fr-graph { + width: 100%; + + pointer-events: none; } diff --git a/style.css b/style.css index 7a64eee..ce45b61 100644 --- a/style.css +++ b/style.css @@ -1,429 +1,869 @@ -noscript { position:absolute; left:0; right:0; font-size:6em; text-align:center; } -html { background:#e5e5e5; } -.graphtool { font-size:initial; color:black; } -.graphBackground { fill:white; } -.main { min-height:100vh; display:flex; flex-direction:column; } -.graphBox { margin:0.25em auto 0.75em; } -#fr-graph { width:calc((100vh - 13.5em) * 800 / 346); max-width:100%; } -svg text { cursor:default; user-select:none; } -.controls { flex:auto; display:flex; flex-flow:row-reverse wrap; } -.manage,.select { flex:1; margin:0 0.8em; font-size:large; } -.select { border-bottom:2px solid #1c2126; } -.select,.selector-panel { display:flex; flex-direction:column; } -.select>div,.selector-panel>div { flex:auto; display:flex; position:relative; } +noscript { + position: absolute; + left: 0; + right: 0; + font-size: 6em; + text-align: center; +} +html { + background: #e5e5e5; +} +.graphtool { + font-size: initial; + color: black; +} +.graphBackground { + fill: white; +} +.main { + min-height: 100vh; + display: flex; + flex-direction: column; +} +.graphBox { + margin: 0.25em auto 0.75em; +} +#fr-graph { + width: calc((100vh - 13.5em) * 800 / 346); + max-width: 100%; +} +svg text { + cursor: default; + user-select: none; +} +.controls { + flex: auto; + display: flex; + flex-flow: row-reverse wrap; +} +.manage, +.select { + flex: 1; + margin: 0 0.8em; + font-size: large; +} +.select { + border-bottom: 2px solid #1c2126; +} +.select, +.selector-panel { + display: flex; + flex-direction: column; +} +.select > div, +.selector-panel > div { + flex: auto; + display: flex; + position: relative; +} .tools { - display:flex; flex-wrap:wrap; justify-content:space-around; - align-items:center; padding:0.2em 0; margin:-0.5em 1vw 0; - border-radius:8px; background:#f0f5f2; font-size: smaller; - border:1px solid #aca; - position:relative; -} -@media only screen and (max-width:70em) { - @media (max-aspect-ratio:5/3) { - .controls { flex-flow:column; } - .manage { flex:0; } - .controls>div.manage { margin-bottom:0.6em; } + display: flex; + flex-wrap: wrap; + justify-content: space-around; + align-items: center; + padding: 0.2em 0; + margin: -0.5em 1vw 0; + border-radius: 8px; + background: #f0f5f2; + font-size: smaller; + border: 1px solid #aca; + position: relative; +} +@media only screen and (max-width: 70em) { + @media (max-aspect-ratio: 5/3) { + .controls { + flex-flow: column; } - @media not (max-aspect-ratio:5/3) , (max-width:40em) { - #fr-graph { margin:0 auto; } - .manage,.select { margin:0; } + .manage { + flex: 0; } -} -@media only screen and (min-aspect-ratio:9/4) { - .main { flex-direction:row; } - .graphBox { margin:auto; } - #fr-graph { height:auto; max-height:calc(100vh - 3em); width:100%; } - .controls { flex-flow:column; } - .manage { flex:0; } - .manage,.select { margin:0; } + .controls > div.manage { + margin-bottom: 0.6em; + } + } + @media not (max-aspect-ratio: 5/3), (max-width: 40em) { + #fr-graph { + margin: 0 auto; + } + .manage, + .select { + margin: 0; + } + } +} +@media only screen and (min-aspect-ratio: 9/4) { + .main { + flex-direction: row; + } + .graphBox { + margin: auto; + } + #fr-graph { + height: auto; + max-height: calc(100vh - 3em); + width: 100%; + } + .controls { + flex-flow: column; + } + .manage { + flex: 0; + } + .manage, + .select { + margin: 0; + } } -path.sample { stroke-width:1.9px; opacity:0.8; } -path.highlight { stroke-width:3.3px; } -tr.highlight { box-shadow:inset 0 0 1.5em 0.2em; } -td { color:initial; vertical-align:middle; } +path.sample { + stroke-width: 1.9px; + opacity: 0.8; +} +path.highlight { + stroke-width: 3.3px; +} +tr.highlight { + box-shadow: inset 0 0 1.5em 0.2em; +} +td { + color: initial; + vertical-align: middle; +} -.dBScaler { opacity:0.22; fill:#404c50; } -.dBScaler circle,.dBScaler rect { cursor:move; } -.dBScaler:hover,.dBScaler.active { opacity:1; } +.dBScaler { + opacity: 0.22; + fill: #404c50; +} +.dBScaler circle, +.dBScaler rect { + cursor: move; +} +.dBScaler:hover, +.dBScaler.active { + opacity: 1; +} -body { margin:0px; } -table { border-collapse: collapse; } -tr,td { padding:0px; } -.manageTable { width:100%; position:relative; z-index:1; } -.manageTable tr:not(.addPhone) { border-bottom: 3px solid #e5e5e5; } +body { + margin: 0px; +} +table { + border-collapse: collapse; +} +tr, +td { + padding: 0px; +} +.manageTable { + width: 100%; + position: relative; + z-index: 1; +} +.manageTable tr:not(.addPhone) { + border-bottom: 3px solid #e5e5e5; +} .remove { - cursor:pointer; text-align:center; color:#44292a; position:relative; - background-size:contain; background-repeat:no-repeat; + cursor: pointer; + text-align: center; + color: #44292a; + position: relative; + background-size: contain; + background-repeat: no-repeat; +} +.remove:hover { + color: #813; +} +span.remove { + float: right; + font-size: 95%; + width: 1em; +} +.remove svg { + width: 0.7em; + position: absolute; + left: 0; + bottom: 0; + user-select: none; +} +.remove svg:hover circle { + fill: #fc0; } -.remove:hover { color:#813; } -span.remove { float:right; font-size:95%; width:1em; } -.remove svg { width:0.7em; position:absolute; left:0; bottom:0; user-select:none; } -.remove svg:hover circle { fill:#fc0; } .addButton, -td.remove { width:1.4em; font-size:120%; padding-left:0.08em; } +td.remove { + width: 1.4em; + font-size: 120%; + padding-left: 0.08em; +} /*.phoneId*/ -.key { width:16%; } -.calibrate { width:2.5em; } -.baselineButton { width:2.5em; } -.hideButton { width:2em; } -.button-download { display: none; } -.lastColumn { width:2.2em; } - -.addPhone { height:1.8em; } +.key { + width: 16%; +} +.calibrate { + width: 2.5em; +} +.baselineButton { + width: 2.5em; +} +.hideButton { + width: 2em; +} +.button-download { + display: none; +} +.lastColumn { + width: 2.2em; +} + +.addPhone { + height: 1.8em; +} .addPhone:after { - border:2px dashed #99a; content:''; box-sizing:border-box; - position:absolute; left:0; right:0; bottom:0; height:calc(1.8em - 2px); - pointer-events:none; -} -.addButton,.addLock { cursor:pointer; text-align:center; color:#445; } -.addButton:hover { color:#371; } -.helpText { color:#667; font-style:italic; font-size:0.7em; padding:0.1em 0.3em 0 0; } -.addPhone.selected { background:#d8fde2; } -.addLock { font-size:0.8em; color:#112; padding-right:0.3em; } -.addLock:hover,.addPhone.locked .addLock { color:#160; } -.addPhone.locked:after { border-color:#371; } + border: 2px dashed #99a; + content: ""; + box-sizing: border-box; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: calc(1.8em - 2px); + pointer-events: none; +} +.addButton, +.addLock { + cursor: pointer; + text-align: center; + color: #445; +} +.addButton:hover { + color: #371; +} +.helpText { + color: #667; + font-style: italic; + font-size: 0.7em; + padding: 0.1em 0.3em 0 0; +} +.addPhone.selected { + background: #d8fde2; +} +.addLock { + font-size: 0.8em; + color: #112; + padding-right: 0.3em; +} +.addLock:hover, +.addPhone.locked .addLock { + color: #160; +} +.addPhone.locked:after { + border-color: #371; +} /* For headphone as opposed to brand in current headphone table */ -.phonename { font-weight:bold; display:inline; position:relative; } -.phonename div, .phonename span { - position:absolute; top:0; white-space:nowrap; - z-index:2; cursor:pointer; height:1.3em; - background:rgba(238,238,238,0.7); +.phonename { + font-weight: bold; + display: inline; + position: relative; +} +.phonename div, +.phonename span { + position: absolute; + top: 0; + white-space: nowrap; + z-index: 2; + cursor: pointer; + height: 1.3em; + background: rgba(238, 238, 238, 0.7); +} +.variantName { + left: -3px; + padding-left: 1px; + border-left: 2px solid #b61; +} +.variantPopout { + opacity: 0.8; +} +.phonename div:hover { + color: #a61; +} + +.keyLine { + max-height: 2.3em; + display: block; + vertical-align: center; } -.variantName { left:-3px; padding-left:1px; border-left:2px solid #b61; } -.variantPopout { opacity:0.8; } -.phonename div:hover { color:#a61; } - -.keyLine { max-height:2.3em; display:block; vertical-align:center; } .keyLine path { - stroke-width:0.3vw; vector-effect:non-scaling-stroke; - fill:none; stroke-linecap:round; stroke-linejoin:round; + stroke-width: 0.3vw; + vector-effect: non-scaling-stroke; + fill: none; + stroke-linecap: round; + stroke-linejoin: round; +} +.keySel rect, +.keySelBoth { + cursor: pointer; } -.keySel rect,.keySelBoth { cursor:pointer; } -.pinMark { float:right; width:80%; margin-right:-8.6%; margin-bottom:0.6em; } -.pinMark path { stroke:#445; } +.pinMark { + float: right; + width: 80%; + margin-right: -8.6%; + margin-bottom: 0.6em; +} +.pinMark path { + stroke: #445; +} -tr { background:#f9fcfc; } -.keyBackground { stroke:#f9fcfc; } -.scrollOuter { flex:1; position:relative; } +tr { + background: #f9fcfc; +} +.keyBackground { + stroke: #f9fcfc; +} +.scrollOuter { + flex: 1; + position: relative; +} .scroll { - position:absolute; left:0; right:0; top:0; bottom:0; - overflow:auto; - display:flex; flex-direction:column; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + overflow: auto; + display: flex; + flex-direction: column; +} +.scroll div { + border-left: 0.3em solid #1c2126; + padding-left: 0.2em; + cursor: pointer; +} +.scroll div:hover, +.scroll div.active { + background: #cccaad; +} +.scroll div.active { + border-left: 0.3em solid #5ce; +} +.scroll div:hover { + border-left: 0.3em solid #fd0; } -.scroll div { border-left:0.3em solid #1c2126; padding-left:0.2em; cursor:pointer; } -.scroll div:hover,.scroll div.active { background:#cccaad; } -.scroll div.active { border-left:0.3em solid #5ce; } -.scroll div:hover { border-left:0.3em solid #fd0; } .search { - flex:none; - padding:2px; padding-left:1.2em; height:1.3em; line-height:1.3em; - border:1px solid #bbb; + flex: none; + padding: 2px; + padding-left: 1.2em; + height: 1.3em; + line-height: 1.3em; + border: 1px solid #bbb; +} +.search, +input[type="number"] { + box-shadow: inset 0 0.1em 0.3em #ccefff; +} +.chevron { + height: 1.3em; + width: 0.9em; + position: absolute; + top: -1.3em; + fill: #1c2126; +} +.stop { + height: 0.35em; + position: absolute; + right: 0; + bottom: 0; + fill: #1c2126; +} +.scroll div { + background: white; } -.search,input[type=number] { box-shadow:inset 0 0.1em 0.3em #ccefff; } -.chevron { height:1.3em; width:0.9em; position:absolute; top:-1.3em; fill:#1c2126; } -.stop { height:0.35em; position:absolute; right:0; bottom:0; fill:#1c2126; } -.scroll div { background:white; } button { - border:none; background:#e0ded1; position:relative; - height:1.5em; - color:#34170b; -} -#download { font-weight:bold; color:#3d130b; } -#expandTools { color:#382d28; } -button:hover,#expandTools:hover,#download:hover { color: #124; } -button:hover:before,button:hover:after { border-color: #9ac; color: #124; } -button:before,button:after { - border:2px groove #e1e8a3; content:''; - position:absolute; top:0; bottom:0; width:0.3em; -} -button:before { border-right:0; left:0; } -button:after { border-left:0; right:0; } -button.selected { background-color: #dd9; } + border: none; + background: #e0ded1; + position: relative; + height: 1.5em; + color: #34170b; +} +#download { + font-weight: bold; + color: #3d130b; +} +#expandTools { + color: #382d28; +} +button:hover, +#expandTools:hover, +#download:hover { + color: #124; +} +button:hover:before, +button:hover:after { + border-color: #9ac; + color: #124; +} +button:before, +button:after { + border: 2px groove #e1e8a3; + content: ""; + position: absolute; + top: 0; + bottom: 0; + width: 0.3em; +} +button:before { + border-right: 0; + left: 0; +} +button:after { + border-left: 0; + right: 0; +} +button.selected { + background-color: #dd9; +} .button { - cursor:default; - color:#261d10; font-size:0.75em; text-align:center; line-height:1.15; - border-left:2px solid #e5e5e5; + cursor: default; + color: #261d10; + font-size: 0.75em; + text-align: center; + line-height: 1.15; + border-left: 2px solid #e5e5e5; +} +.button:hover { + color: #137; +} +.button.selected { + background-color: #e8ed82; } -.button:hover { color: #137; } -.button.selected { background-color: #e8ed82; } -.button svg { display:block; margin:auto 0em; } -.button.hideIcon { color: #989381; } -.button.hideIcon.selected { background:none; color: #b76138; } -.button.hideIcon:hover { color: #359; } +.button svg { + display: block; + margin: auto 0em; +} +.button.hideIcon { + color: #989381; +} +.button.hideIcon.selected { + background: none; + color: #b76138; +} +.button.hideIcon:hover { + color: #359; +} -.keyOnly { fill:#555049; } -.keyOnly:hover { fill:#124; } -.imbalance { fill:#e11; font-weight:bold; pointer-events:none; } +.keyOnly { + fill: #555049; +} +.keyOnly:hover { + fill: #124; +} +.imbalance { + fill: #e11; + font-weight: bold; + pointer-events: none; +} -input[type=number] { width:2.3em; border:1px solid #899174; height:1.5em; } -.curves input[type=number] { display:block; margin-left:auto; margin-right:auto; } +input[type="number"] { + width: 2.3em; + border: 1px solid #899174; + height: 1.5em; +} +.curves input[type="number"] { + display: block; + margin-left: auto; + margin-right: auto; +} .variants { - display:flex; align-items:center; - float:right; margin-right:0.3em; - color:#3d332a; cursor:pointer; outline:none; -} -.variants svg { width:0.95em; } -.variants:hover { color:#124; } - -.lineLabel { pointer-events:none; } -.lineLabel rect { fill:rgba(255,255,255,0.6); } - -.inspector { pointer-events:none; } -.inspector line { stroke-width:0.8px; stroke:#266149; } - -.tools * { display:inline-block; } -.zoom button { min-width:4em; } -.tools div { color:#170b08; } -.normalize span:not(.helptip) { padding-right:0.3em; } -#norm-phon { width:2.8em; } -#norm-fr { width:4em; } -.tools input:invalid { background:#fbb; } -.normalize div.selected { margin-bottom:-3px; border-bottom:3px solid #261811; background:#cde4aa; } -.normalize div span { cursor:pointer; } -#expandTools { display:none; height:1.5em; } - -@media only screen and (max-width:60em) { - .tools { padding-right:3em; } - #expandTools { display:inline-block; position:absolute; right:1em; top:0.2em; } - .tools.collapseTools .normalize, - .tools.collapseTools .smooth, - .tools.collapseTools .miscTools button:not(#download) { display:none; } - @media screen and (max-width:40em) { - .tools.collapseTools .zoom { display:none; } + display: flex; + align-items: center; + float: right; + margin-right: 0.3em; + color: #3d332a; + cursor: pointer; + outline: none; +} +.variants svg { + width: 0.95em; +} +.variants:hover { + color: #124; +} + +.lineLabel { + pointer-events: none; +} +.lineLabel rect { + fill: rgba(255, 255, 255, 0.6); +} + +.inspector { + pointer-events: none; +} +.inspector line { + stroke-width: 0.8px; + stroke: #266149; +} + +.tools * { + display: inline-block; +} +.zoom button { + min-width: 4em; +} +.tools div { + color: #170b08; +} +.normalize span:not(.helptip) { + padding-right: 0.3em; +} +#norm-phon { + width: 2.8em; +} +#norm-fr { + width: 4em; +} +.tools input:invalid { + background: #fbb; +} +.normalize div.selected { + margin-bottom: -3px; + border-bottom: 3px solid #261811; + background: #cde4aa; +} +.normalize div span { + cursor: pointer; +} +#expandTools { + display: none; + height: 1.5em; +} + +@media only screen and (max-width: 60em) { + .tools { + padding-right: 3em; + } + #expandTools { + display: inline-block; + position: absolute; + right: 1em; + top: 0.2em; + } + .tools.collapseTools .normalize, + .tools.collapseTools .smooth, + .tools.collapseTools .miscTools button:not(#download) { + display: none; + } + @media screen and (max-width: 40em) { + .tools.collapseTools .zoom { + display: none; } - .targets.collapseTools { display:none; } + } + .targets.collapseTools { + display: none; + } } .helptip { - position:relative; margin-left:-0.15em; - width:1em; height:1em; border-radius:50%; - background:#b3bfba; border:0.15em solid #b3bfba; - text-align:center; font-weight:bold; cursor:default; - color:#f0f5f2; + position: relative; + margin-left: -0.15em; + width: 1em; + height: 1em; + border-radius: 50%; + background: #b3bfba; + border: 0.15em solid #b3bfba; + text-align: center; + font-weight: bold; + cursor: default; + color: #f0f5f2; } .helptip span { - visibility:hidden; - position:absolute; left:70%; bottom:70%; z-index:8; - width:16rem; max-width:100vw; padding:0.5em; - border-radius:0.6em; border-bottom-left-radius:0; - background-color:#1f2120; font-weight:normal; -} -.helptip:hover span,.helptip.active span { visibility:visible; } -@media only screen and (max-width:40em) { - .helptip { position:static; } - .helptip span { - left:0; bottom:0; width:calc(100% - 1.25em); - border-bottom-left-radius:0.6em; - } + visibility: hidden; + position: absolute; + left: 70%; + bottom: 70%; + z-index: 8; + width: 16rem; + max-width: 100vw; + padding: 0.5em; + border-radius: 0.6em; + border-bottom-left-radius: 0; + background-color: #1f2120; + font-weight: normal; +} +.helptip:hover span, +.helptip.active span { + visibility: visible; +} +@media only screen and (max-width: 40em) { + .helptip { + position: static; + } + .helptip span { + left: 0; + bottom: 0; + width: calc(100% - 1.25em); + border-bottom-left-radius: 0.6em; + } } .targetClass { - display:flex; flex-grow:1; flex-wrap:wrap; position:relative; - padding:0.1em 0 0.4em; justify-content:space-around; - background:#f6f2f3; border-radius:7px; margin:0 2px; - border:1px solid #bab; + display: flex; + flex-grow: 1; + flex-wrap: wrap; + position: relative; + padding: 0.1em 0 0.4em; + justify-content: space-around; + background: #f6f2f3; + border-radius: 7px; + margin: 0 2px; + border: 1px solid #bab; } .targets { - display:flex; flex-wrap:none; position:relative; - width:100%; font-size:smaller; margin-bottom:6px; - padding-bottom:1em; + display: flex; + flex-wrap: none; + position: relative; + width: 100%; + font-size: smaller; + margin-bottom: 6px; + padding-bottom: 1em; } .targetLabel { - position:absolute; bottom:0; left:0; right:0; text-align:center; - font-size:0.8em; color:#5f5853; - z-index:1; + position: absolute; + bottom: 0; + left: 0; + right: 0; + text-align: center; + font-size: 0.8em; + color: #5f5853; + z-index: 1; +} +.targetClass .targetLabel span { + background: #fbeef8; + border-radius: 2px; +} +.targetClass .targetLabel { + bottom: -0.7em; +} +.target span { + font-size: 1.1em; +} +.target { + cursor: pointer; + z-index: 1; + text-decoration: underline #736960; + color: #1c1307; + border-radius: 2px; +} +.target:hover { + background: #eef5bc; + text-decoration-color: #665f1e; +} + +.cash, +.cashMessage, +.fadeAll { + position: fixed; + text-align: center; + z-index: 10; } -.targetClass .targetLabel span { background:#fbeef8; border-radius:2px; } -.targetClass .targetLabel { bottom:-0.7em; } -.target span { font-size:1.1em; } -.target { cursor:pointer; z-index:1; text-decoration:underline #736960; color:#1c1307; border-radius:2px; } -.target:hover { background:#eef5bc; text-decoration-color:#665f1e; } - -.cash,.cashMessage,.fadeAll { position:fixed; text-align:center; z-index:10; } .cashMessage { - height:auto; top:calc(50vmin - 7em); - width:30em; left:calc(50vw - 20em); - padding:0.5em 5em 2.5em; line-height:1.5; - background:white; box-shadow:0 0 20px 6px #444; + height: auto; + top: calc(50vmin - 7em); + width: 30em; + left: calc(50vw - 20em); + padding: 0.5em 5em 2.5em; + line-height: 1.5; + background: white; + box-shadow: 0 0 20px 6px #444; +} +@media only screen and (max-width: 40em) { + .cashMessage { + width: calc(100vw - 10em); + left: 0; + } } -@media only screen and (max-width:40em) { - .cashMessage { width:calc(100vw - 10em); left:0; } +.fadeAll, +.cash { + left: 0; + right: 0; +} +.fadeAll { + z-index: 9; + top: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.3); +} +.cashMessage button { + width: 50%; + margin: 0px auto; +} +.cash { + font-size: 30em; + top: calc(50vmin - 0.6em); + pointer-events: none; } -.fadeAll,.cash { left:0; right:0; } -.fadeAll { z-index:9; top:0; bottom:0; background:rgba(255,255,255,0.3); } -.cashMessage button { width:50%; margin:0px auto; } -.cash { font-size:30em; top:calc(50vmin - 0.6em); pointer-events:none; } - - /***** Restoring original layout after DOM changes *****/ /* Note to self: Graph aspect ratio is 800:346 */ body { - opacity: 1.0; + opacity: 1; } main.main { - position: relative; + position: relative; } section.parts-primary, section.parts-secondary { - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-rows: calc((100vw * 0.4325) + 3em) 1fr; - - width: 100vw; - height: 100vh; -} + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: calc((100vw * 0.4325) + 3em) 1fr; + width: 100vw; + height: 100vh; +} section.parts-primary, section.parts-secondary { - position: absolute; - top: 0; - left: 0; + position: absolute; + top: 0; + left: 0; } section.parts-primary { } div.graphBox { - grid-column-start: 1; - grid-column-end: 3; - grid-row-start: 1; - grid-row-end: 2; - - width: 100%; + grid-column-start: 1; + grid-column-end: 3; + grid-row-start: 1; + grid-row-end: 2; + + width: 100%; } svg#fr-graph { - width: 100%; + width: 100%; } div.manage { - grid-column-start: 2; - grid-column-end: 3; - grid-row-start: 2; - grid-row-end: 3; - - max-height: 100%; - margin: 0 0.8em; + grid-column-start: 2; + grid-column-end: 3; + grid-row-start: 2; + grid-row-end: 3; + + max-height: 100%; + margin: 0 0.8em; } td.item-phone span.brand:after { - content: ' '; - white-space: pre; + content: " "; + white-space: pre; } section.parts-secondary { - pointer-events: none; + pointer-events: none; } div.controls { - grid-column-start: 1; - grid-column-end: 2; - grid-row-start: 2; - grid-row-end: 3; - - pointer-events: all; + grid-column-start: 1; + grid-column-end: 2; + grid-row-start: 2; + grid-row-end: 3; + + pointer-events: all; } div.select { - position: relative; + position: relative; } svg.chevron { - top: 0px; + top: 0px; } svg.stop { - z-index: 1; + z-index: 1; } div.scroll { - background-color: #fff; + background-color: #fff; } div.phone-item { - display: flex; + display: flex; } div.phone-item-add { - margin-left: auto; - border: none; - padding-left: 0px; + margin-left: auto; + border: none; + padding-left: 0px; } -@media ( max-height: 60vw ) { - section.parts-primary, - section.parts-secondary { - grid-template-rows: calc((100vw * 0.4325) + 3em) 1fr; - grid-template-rows: calc(100vh - 10.625em) 1fr; - } - - div.graphBox { - max-width: calc((100vh - 10.625em - 3em) * (800/346)); - } +@media (max-height: 60vw) { + section.parts-primary, + section.parts-secondary { + grid-template-rows: calc((100vw * 0.4325) + 3em) 1fr; + grid-template-rows: calc(100vh - 10.625em) 1fr; + } + + div.graphBox { + max-width: calc((100vh - 10.625em - 3em) * (800 / 346)); + } } -@media ( max-width: 800px ) { - main.main { - position: relative; - - display: flex; - flex-direction: column; - } +@media (max-width: 800px) { + main.main { + position: relative; - section.parts-primary, - section.parts-secondary { - position: relative; - - display: block; + display: flex; + flex-direction: column; + } - width: 100vw; - height: auto; - } - - section.parts-primary { - flex: auto 0 0; - } - - section.parts-secondary { - display: flex; - flex-direction: column; - - flex: auto 1 1; - } - - div.controls { - flex: 300px 1 0; - } - - div.select { - height: 100%; - min-height: 300px; - } + section.parts-primary, + section.parts-secondary { + position: relative; + + display: block; + + width: 100vw; + height: auto; + } + + section.parts-primary { + flex: auto 0 0; + } + + section.parts-secondary { + display: flex; + flex-direction: column; + + flex: auto 1 1; + } + + div.controls { + flex: 300px 1 0; + } + + div.select { + height: 100%; + min-height: 300px; + } } /* Hide second remove button. This is a temporary fix until second remove button bug is fixed */ div.phone-item > span.remove { - display: none; + display: none; } - - /***** Hiding elements from alt layout *****/ @@ -434,5 +874,5 @@ div.copy-url, td.curve-color, div.expand-collapse, button#theme { - display: none; + display: none; } diff --git a/styles/dark.css b/styles/dark.css index 06d7c54..bb14551 100644 --- a/styles/dark.css +++ b/styles/dark.css @@ -1,16 +1,51 @@ -html,.addPhone { background:#191b1f; } -.manageTable tr:not(.addPhone) { border-bottom-color:#191b1f; } -.button { border-left-color:#191b1f; } -.tools { background:#dce3df; } -.addPhone.selected { background:#0c2716; } -.addLock { color:#778; } -.addPhone.locked:after { border-color:#375b0b; } -tr { background:#d7e0e0; } -.keyBackground { stroke:#d7e0e0; } -.search { background:#e3e2e1; } -.pinMark path { stroke:#726d63; } -.scroll div { border-left-color:#101112; } -.select { border-bottom-color:#101112; } -.chevron,.stop { fill:#101112; } -.scroll div { background:#ebe9e6; } -.targetClass,.targetClass .targetLabel span { background:#e3dfe0; } +html, +.addPhone { + background: #191b1f; +} +.manageTable tr:not(.addPhone) { + border-bottom-color: #191b1f; +} +.button { + border-left-color: #191b1f; +} +.tools { + background: #dce3df; +} +.addPhone.selected { + background: #0c2716; +} +.addLock { + color: #778; +} +.addPhone.locked:after { + border-color: #375b0b; +} +tr { + background: #d7e0e0; +} +.keyBackground { + stroke: #d7e0e0; +} +.search { + background: #e3e2e1; +} +.pinMark path { + stroke: #726d63; +} +.scroll div { + border-left-color: #101112; +} +.select { + border-bottom-color: #101112; +} +.chevron, +.stop { + fill: #101112; +} +.scroll div { + background: #ebe9e6; +} +.targetClass, +.targetClass .targetLabel span { + background: #e3dfe0; +} diff --git a/styles/white.css b/styles/white.css index 960d1db..2c459c3 100644 --- a/styles/white.css +++ b/styles/white.css @@ -1,10 +1,33 @@ -html,.addPhone { background:white; } -.manageTable tr:not(.addPhone) { border-bottom-color: white; } -.button { border-left-color:white; } -.tools { background:#ebf0ed; } -tr { background:#ebeeee; } -.keyBackground { stroke:#ebeeee; } -button { background:#dddbce; } -button:before,button:after { border-color:#dde4a0; } -.button.selected { background-color: #e4eb6c; } -.normalize div.selected { border-bottom-color:#21150f; background:#cde4aa; } +html, +.addPhone { + background: white; +} +.manageTable tr:not(.addPhone) { + border-bottom-color: white; +} +.button { + border-left-color: white; +} +.tools { + background: #ebf0ed; +} +tr { + background: #ebeeee; +} +.keyBackground { + stroke: #ebeeee; +} +button { + background: #dddbce; +} +button:before, +button:after { + border-color: #dde4a0; +} +.button.selected { + background-color: #e4eb6c; +} +.normalize div.selected { + border-bottom-color: #21150f; + background: #cde4aa; +}