From 63fdbdbaaf309ab54b05cba04fbacc63f106f423 Mon Sep 17 00:00:00 2001 From: HarutoHiroki Date: Tue, 17 Sep 2024 12:20:42 -0500 Subject: [PATCH] Public release of new features - Downloadable CSV of all active graphs - Per page Y scaling (requested by listener) - Added a Graph Customisation menu --- README.md | 3 + assets/css/extra.css | 167 ++++++++- assets/css/reinvented-color-wheel.min.css | 1 + assets/js/config.js | 56 ++- assets/js/config_hp.js | 33 +- assets/js/graphtool.js | 408 +++++++++++++++++----- assets/js/reinvented-color-wheel.min.js | 43 +++ headphones.html | 2 + index.html | 2 + 9 files changed, 579 insertions(+), 136 deletions(-) create mode 100644 assets/css/reinvented-color-wheel.min.css create mode 100644 assets/js/reinvented-color-wheel.min.js diff --git a/README.md b/README.md index 95feb9a..5b15844 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,9 @@ https://graphtool-demo.harutohiroki.com/ - Per-measurement compensation (requested by listener) - Added support for Haruto's Graph Extension to apply eq to browserwide - Made Preference Bounds better and not relying on a png anymore +- Downloadable CSV of all active graphs +- Per page Y scaling (requested by listener) +- Added a Graph Customisation menu # TODO diff --git a/assets/css/extra.css b/assets/css/extra.css index bf488dc..25302d5 100644 --- a/assets/css/extra.css +++ b/assets/css/extra.css @@ -84,6 +84,57 @@ image.graph_logo { filter: var(--svg-filter); } +/** Color Picker Thingy **/ +.colorStylePicker { + display: flex; + align-items: flex-start; + box-sizing: border-box; + flex-wrap: wrap; + flex-direction: row; + background-color: var(--background-color); + border: 1px solid var(--background-color-contrast-more); + border-radius: 6px; + z-index: 1000; +} + +.colorStylePicker .left-side { + display: flex; + align-items: center; + padding: 0 10px 0 0; +} + +.colorStylePicker .right-side { + flex: 1; + align-items: center; +} + +.colorStylePicker .right-side .row { + display: flex; + flex-direction: row; +} + + +.colorStylePicker .right-side>div>span { + color: var(--font-color-inputs); + font-family: var(--font-primary); + font-size: 13.5px; + line-height: 1em; +} + +.tickText { + order: 1 !important; +} +.tickInput { + order: 2 !important; + width: 55px !important; +} +.spaceText { + order: 3 !important; +} +.spaceInput { + order: 4 !important; + width: 55px !important; +} /** Custom DF tilt**/ @@ -125,7 +176,8 @@ div.customDF>button+div { margin-left: 6px } -div.customDF>div>input { +div.customDF>div>input, +.colorStylePicker input { order: 2; box-sizing: border-box; width: 70px; @@ -146,7 +198,16 @@ div.customDF>div>input { padding-left: 11px } -div.customDF>div:after { +.colorStylePicker input { + border: 1px solid var(--background-color-contrast-more); + border-radius: 6px; + border-left: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + height: 35.6px; +} + +div.customDF>div:after{ order: 3; content: ''; box-sizing: border-box; @@ -159,7 +220,8 @@ div.customDF>div:after { border-radius: 0 6px 6px 0 } -div.customDF>div>span { +div.customDF>div>span, +.colorStylePicker .right-side>.row>span { order: 1; padding: 11px 16px; background-color: var(--background-color)!important; @@ -179,7 +241,8 @@ div.customDF>div.selected>span { color: var(--font-color-secondary) } -div.customDF>button { +div.customDF>button, +.colorStylePicker button { padding: 11px 16px; background-color: var(--background-color) !important; @@ -194,8 +257,14 @@ div.customDF>button { white-space: nowrap; cursor: pointer; } +.colorStylePicker button { + order: 3; + margin-left: 7px; + height: 35.6px; +} -div.customDF>button:active { +div.customDF>button:active, +.colorStylePicker button:active { box-sizing: border-box; background-color: var(--accent-color) !important; border-color: var(--accent-color) !important; @@ -203,12 +272,32 @@ div.customDF>button:active { color: var(--font-color-secondary); } -div.customDF>button.selected { +div.customDF>button.selected, +.colorStylePicker button.selected { background-color: var(--accent-color)!important; border-color: var(--accent-color); color: var(--font-color-secondary) } +.colorStylePicker select { + box-sizing: border-box; + width: 120px; + height: 36px; + padding: 8px 6px 8px 0px; + + 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; +} + /** extra panel **/ div.extra-panel { @@ -650,4 +739,70 @@ tbody.curves > tr > td.comp select { tbody.curves > tr > td.button-pin { order: 5; } +} + +tbody.curves > tr > td.remove { + order: 7 !important; +} + +:root { + --icon-save: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M16 11h5l-9 10-9-10h5v-11h8v11zm1 11h-10v2h10v-2z'/%3E%3C/svg%3E"); +} + +tbody.curves > tr > td.button-saveSquig { + order: 6; +} + +tbody.curves > tr > td.button.button-saveSquig { + mask: var(--icon-save); + -webkit-mask: var(--icon-save); + mask-size: 15px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 15px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; +} + +tbody.curves > tr > td.button-saveSquig:before { + background-color: var(--background-color-contrast); + border-color: transparent; +} + +tbody.curves > tr > td.button-saveSquig:after { + background-color: var(--font-color-primary) !important; +} + +/** color and line weight / line dash style menu **/ +.line-style-menu { + display: flex; + align-items: center; + gap: 10px; + display: inline-block; +} + +.line-style-menu-content { + display: none; + position: absolute; + background-color: var(--background-color); + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 1; +} + +.line-style-menu-content .style-button { + color: var(--font-color-primary); + padding: 12px 16px; + text-decoration: none; + display: block; + cursor: pointer; +} + +.line-style-menu-content input[type="squig_color"] { + width: 100%; + height: 100%; + padding: 0; + margin: 0; + border: none; + cursor: pointer; } \ No newline at end of file diff --git a/assets/css/reinvented-color-wheel.min.css b/assets/css/reinvented-color-wheel.min.css new file mode 100644 index 0000000..fc7d52f --- /dev/null +++ b/assets/css/reinvented-color-wheel.min.css @@ -0,0 +1 @@ +.reinvented-color-wheel,.reinvented-color-wheel--hue-handle,.reinvented-color-wheel--hue-wheel,.reinvented-color-wheel--sv-handle,.reinvented-color-wheel--sv-space{touch-action:manipulation;touch-action:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.reinvented-color-wheel{position:relative;display:inline-block;line-height:0;border-radius:50%}.reinvented-color-wheel--hue-wheel{border-radius:50%}.reinvented-color-wheel--sv-space{position:absolute;left:0;top:0;right:0;bottom:0;margin:auto}.reinvented-color-wheel--hue-handle,.reinvented-color-wheel--sv-handle{position:absolute;box-sizing:border-box;border-radius:50%;border:2px solid #fff;box-shadow:0 0 0 1px #000 inset}.reinvented-color-wheel--hue-handle{pointer-events:none} \ No newline at end of file diff --git a/assets/js/config.js b/assets/js/config.js index 254c2bd..3a09247 100644 --- a/assets/js/config.js +++ b/assets/js/config.js @@ -43,19 +43,21 @@ const targets = [ ]; // Haruto's Addons -const preference_bounds_name = "Preference Bounds RAW", // Preference bounds name - preference_bounds_dir = "assets/pref_bounds/", // Preference bounds directory - preference_bounds_startup = false, // If true, preference bounds are displayed on startup - PHONE_BOOK = "phone_book.json", // Path to phone book JSON file - default_DF_name = "KEMAR DF", // Default RAW DF name - dfBaseline = true, // If true, DF is used as baseline when custom df tilt is on - default_bass_shelf = 8, // Default Custom DF bass shelf value - default_tilt = -0.8, // Default Custom DF tilt value - default_ear = 0, // Default Custom DF ear gain value - default_treble = 0, // Default Custom DF treble gain value - tiltableTargets = ["KEMAR DF"], // Targets that are allowed to be tilted - compTargets = ["KEMAR DF"], // Targets that are allowed to be used for compensation - allowCreatorSupport = true; // Allow the creator to have a button top right to support them +const preference_bounds_name = "Preference Bounds RAW", // Preference bounds name + preference_bounds_dir = "assets/pref_bounds/", // Preference bounds directory + preference_bounds_startup = false, // If true, preference bounds are displayed on startup + allowSquigDownload = false, // If true, allows download of measurement data + PHONE_BOOK = "phone_book.json", // Path to phone book JSON file + default_y_scale = "40db", // Default Y scale; values: ["20db", "30db", "40db", "50db", "crin"] + default_DF_name = "KEMAR DF", // Default RAW DF name + dfBaseline = true, // If true, DF is used as baseline when custom df tilt is on + default_bass_shelf = 8, // Default Custom DF bass shelf value + default_tilt = -0.8, // Default Custom DF tilt value + default_ear = 0, // Default Custom DF ear gain value + default_treble = 0, // Default Custom DF treble gain value + tiltableTargets = ["KEMAR DF"], // Targets that are allowed to be tilted + compTargets = ["KEMAR DF"], // Targets that are allowed to be used for compensation + allowCreatorSupport = true; // Allow the creator to have a button top right to support them // ************************************************************* // Functions to support config options set above; probably don't need to change these @@ -129,7 +131,7 @@ setLayout(); const // Short text, center-aligned, useful for a little side info, credits, links to measurement setup, etc. simpleAbout = ` -

This graph database is maintained by HarutoHiroki with frequency responses generated via an "IEC60318-4"-compliant ear simulator. This web software is based on the CrinGraph open source software project, with Audio Spectrum's definition source.

+

This graph database is maintained by HarutoHiroki with frequency responses generated via an "IEC60318-4"-compliant ear simulator. This web software is based on a heavily modified version of the CrinGraph open source software project, with Audio Spectrum's definition source.

`, // Which of the above variables to actually insert into the page whichAccessoriesToUse = simpleAbout; @@ -145,6 +147,18 @@ const linkSets = [ 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/" @@ -158,9 +172,13 @@ const linkSets = [ url: "https://squig.link/" }, { - name: "Timmy", + name: "Timmy (Gizaudio)", url: "https://timmyv.squig.link/" }, + { + name: "Rohsa", + url: "https://rohsa.gitlab.io/graphtool/" + }, ] }, { @@ -221,7 +239,7 @@ let headerLogoText = "HarutoHiroki", }, { name: "Donate", - url: "https://www.paypal.me/harutohirokiUS" + url: "https://ko-fi.com/harutohiroki" }, // { // name: "GitHub", @@ -259,12 +277,12 @@ let tutorialDefinitions = [ { name: 'Presence', width: '5.9%', - description: 'The Presence range is responsible for the clarity and definition of a sound. Over-boosting can cause an irritating, harsh sound. Cutting in this range makes the sound more distant and transparent.' + description: 'The presence range is responsible for the clarity and definition of a sound. Over-boosting can cause an irritating, harsh sound. Cutting in this range makes the sound more distant and transparent.' }, { - name: 'Treble', + name: 'Brilliance', width: '17.4%', - description: 'The Treble range is composed entirely of harmonics and is responsible for sparkle and air of a sound. Over boosting in this region can accentuate hiss and cause ear fatigue.' + description: 'The brilliance range is composed entirely of harmonics and is responsible for sparkle and air of a sound. Over boosting in this region can accentuate hiss and cause ear fatigue.' } ] diff --git a/assets/js/config_hp.js b/assets/js/config_hp.js index 478ee74..66b99d2 100644 --- a/assets/js/config_hp.js +++ b/assets/js/config_hp.js @@ -35,7 +35,6 @@ const init_phones = ["IEF Neutral Target"], // Optio extraEQBandsMax = 20, // Max EQ bands available num_samples = 5, // Number of samples to average for smoothing scale_smoothing = 0.2; // Smoothing factor for scale transitions - // Specify which targets to display const targets = [ @@ -46,17 +45,19 @@ const targets = [ // Haruto's Addons const preference_bounds_name = "Preference Bounds RAW", // Preference bounds name preference_bounds_dir = "assets/pref_bounds/", // Preference bounds directory - preference_bounds_startup = false, // If true, preference bounds are displayed on startup - PHONE_BOOK = "phone_book_hp.json", // Phone book file path & name - default_DF_name = "KEMAR DF", // Default RAW DF name - dfBaseline = true, // If true, DF is used as baseline when custom df tilt is on - default_bass_shelf = 8, // Default Custom DF bass shelf value - default_tilt = -0.8, // Default Custom DF tilt value - default_ear = 0, // Default Custom DF ear gain value - default_treble = 0, // Default Custom DF treble gain value - tiltableTargets = ["KEMAR DF"], // Targets that are allowed to be tilted - compTargets = ["KEMAR DF"], // Targets that are allowed to be used for compensation - allowCreatorSupport = true; // Allow the creator to have a button top right to support them + preference_bounds_startup = false, // If true, preference bounds are displayed on startup + allowSquigDownload = false, // If true, allows download of measurement data + PHONE_BOOK = "phone_book_hp.json", // Path to phone book JSON file + default_y_scale = "40db", // Default Y scale; values: ["20db", "30db", "40db", "50db", "crin"] + default_DF_name = "KEMAR DF", // Default RAW DF name + dfBaseline = true, // If true, DF is used as baseline when custom df tilt is on + default_bass_shelf = 8, // Default Custom DF bass shelf value + default_tilt = -0.8, // Default Custom DF tilt value + default_ear = 0, // Default Custom DF ear gain value + default_treble = 0, // Default Custom DF treble gain value + tiltableTargets = ["KEMAR DF"], // Targets that are allowed to be tilted + compTargets = ["KEMAR DF"], // Targets that are allowed to be used for compensation + allowCreatorSupport = true; // Allow the creator to have a button top right to support them @@ -132,7 +133,7 @@ setLayout(); 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. Audio Spectrum definition source.

+

This graph database is maintained by HarutoHiroki with frequency responses generated via an "IEC60318-4"-compliant ear simulator. This web software is based on a heavily modified version of the CrinGraph open source software project, with Audio Spectrum's definition source.

`, // Which of the above variables to actually insert into the page whichAccessoriesToUse = simpleAbout; @@ -193,6 +194,10 @@ const linkSets = [ name: "In-Ear Fidelity", url: "https://crinacle.com/graphs/headphones/graphtool/" }, + { + name: "Listener", + url: "https://listener800.github.io/" + }, { name: "Super* Review", url: "https://squig.link/hp.html" @@ -236,7 +241,7 @@ let headerLogoText = "HarutoHiroki", }, { name: "Donate", - url: "https://www.paypal.me/harutohirokiUS" + url: "https://ko-fi.com/harutohiroki" }, // { // name: "GitHub", diff --git a/assets/js/graphtool.js b/assets/js/graphtool.js index 8f91751..b79b322 100644 --- a/assets/js/graphtool.js +++ b/assets/js/graphtool.js @@ -17,6 +17,9 @@ doc.html(` PIN + + + @@ -88,7 +91,7 @@ doc.html(`
- Custom Delta Tilt: + Preference Adjustments:
Tilt (dB/Oct) @@ -105,7 +108,7 @@ doc.html(` Ear Gain (dB)
- +
@@ -320,8 +323,8 @@ let boost = default_bass_shelf; let tilt = default_tilt; let ear = default_ear; let treble = default_treble; -let df, dfBase, customTiltName; -let updateDF, prepPrefBounds; +let df, dfBase, updateDF, prepPrefBounds; +let customTiltName = default_DF_name; // Scales let x = d3.scaleLog() @@ -554,46 +557,56 @@ function updateBoundsScaling(h) { updateBoundsScaling(dB.H); doc.select("#yscalebtn").on("click", function() { - // 3 way button, class="crin" is the default, class="40db" is the 40dB scale, class="50db" is the 50dB scale - // 5 way now cuz listener said so - switch (this.classList[0]) { + changeScaling(this.classList[0]); +}); + +function changeScaling(y_scale) { + let button = document.querySelector("#yscalebtn"); + switch (y_scale) { case "20db": - this.classList.remove("20db"); - this.classList.add("30db"); - this.innerHTML = "30dB"; + button.className = "30db"; + button.innerHTML = "30dB"; updateYScaling(101.33, 172); break; case "30db": - this.classList.remove("30db"); - this.classList.add("40db"); - this.innerHTML = "40dB"; + button.className = "40db"; + button.innerHTML = "40dB"; updateYScaling(dB.H, defY); break; case "40db": - this.classList.remove("40db"); - this.classList.add("50db"); - this.innerHTML = "50dB"; + button.className = "50db"; + button.innerHTML = "50dB"; updateYScaling(60.79, 172); break; case "50db": - this.classList.remove("50db"); - this.classList.add("crin"); - this.innerHTML = "Crin"; + //button.classList.remove("50db"); + button.className = "crin"; + button.innerHTML = "Crin"; updateYScaling(54.77, 156.94); break; case "crin": - this.classList.remove("crin"); - this.classList.add("20db"); - this.innerHTML = "20dB"; + //button.classList.remove("crin"); + button.className = "20db"; + button.innerHTML = "20dB"; updateYScaling(152, 172); break; default: - this.className = "40db"; - this.innerHTML = "40dB"; + button.className = "40db"; + button.innerHTML = "40dB"; updateYScaling(dB.H, defY); break; } -}); +} + +// check if user defined a default y scale +function checkUserDefaultScale() { + let validScales = ["20db", "30db", "40db", "50db", "crin"]; + if (default_y_scale && validScales.includes(default_y_scale)) { + let fauxIndex = validScales.indexOf(default_y_scale); + let trueIndex = fauxIndex === 0 ? validScales.length - 1 : fauxIndex - 1; + changeScaling(validScales[trueIndex]); + } +} // Label drawing and screenshot @@ -750,12 +763,8 @@ function saveGraph(ext) { gpath.selectAll("path").classed("highlight",false); drawLabels(); showControls(false); - gr.select("[id=background]").attrs({"display":"none"}); fn(gr.node(), "graph."+ext, {scale:3}) - .then(()=>{ - showControls(true) - gr.select("[id=background]").attrs({"display":null}); - }); + .then(()=>showControls(true)); // Analytics event if (analyticsEnabled) { pushEventTag("clicked_download", targetWindow); } @@ -1013,7 +1022,7 @@ let baseline0 = { p:null, l:null, fn:l=>l }, let gpath = gr.insert("g",".dBScaler") .attr("fill","none") - .attr("stroke-width",2.3) + .attr("stroke-width",2.1) .attr("mask","url(#graphFade)"); function hl(p, h) { gpath.selectAll("path").filter(c=>c.p===p).classed("highlight",h); @@ -1021,24 +1030,31 @@ function hl(p, h) { let table = doc.select(".curves"); let ld_p1 = 1.1673039782614187; -function getCurveColor(id, o) { +function getCurveColor(id, o, hex) { 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) + (o * 30), // hue varies with "o" - 88+30*(j%1 + 1.3*s - t/p3), - 55); //constant luminance -} -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); + if (hex) { + let rgb = d3.rgb(hex); + let hcl = d3.hcl(rgb); + hcl.h = hcl.h + -30 * (o <= 0 ? 0 : 1); // Haruto's Bias, if you want to make the color more dicernable, change the 30 to 60, for the original, change the entire thing to 30 * o + return hcl; + } else { + 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) + (o * 30), // hue varies with "o" + 88+30*(j%1 + 1.3*s - t/p3), + 64); //constant luminance + } +} +let getColor_AC = c => getCurveColor(c.p.id, c.o, c.p.hexColor); +let getColor_ph = (p,i) => getCurveColor(p.id, p.activeCurves[i].o, p.hexColor); +function getDivColor(id, active, hex) { + let c = getCurveColor(id, 0, hex); c.l = 100-(80-Math.min(c.l,60))/(active?1.5:3); c.c = (c.c-20)/(active?3:4); return c; @@ -1051,9 +1067,9 @@ function color_curveToText(c) { return c; } let getTooltipColor = curve => color_curveToText(getColor_AC(curve)); -let getTextColor = p => color_curveToText(getCurveColor(p.id,0)); +let getTextColor = p => color_curveToText(getCurveColor(p.id,0,p.hexColor)); let getBgColor = p => { - let c=getCurveColor(p.id,0).rgb(); + let c=getCurveColor(p.id,0,p.hexColor).rgb(); ['r','g','b'].forEach(p=>c[p]=255-(255-Math.max(0,c[p]))*0.85); return c; } @@ -1095,8 +1111,8 @@ function setPhoneTr(phtr) { 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.style("background",p=>p.isTarget&&!p.active?null:getDivColor(p.id,p.highlight,p.hexColor)) + .style("border-color",p=>p.highlight?getDivColor(p.id,1,p.hexColor):null); phtr.filter(p=>!p.isTarget) .select(".phone-item-add") .selectAll(".remove").data(p=>p.highlight?[p]:[]) @@ -1250,13 +1266,26 @@ 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 || c.p.isPrefBounds) - .attr("class", "target"); - if (targetDashed) t.style("stroke-dasharray", "6, 3"); + let graphLines = p.join("path").attr("opacity", c=>c.p.hide?0:null) + .classed("sample", c=>c.p.samp); + graphLines.attr("stroke", getColor_AC).call(redrawLine); + let t = graphLines.filter(c=>c.p.isTarget) + .attr("class", "target") + .style("stroke-dasharray", "6, 3"); + let pfb = graphLines.filter(c=>c.p.isPrefBounds) + .style("stroke-dasharray", "6, 3"); + if (targetColorCustom) t.attr("stroke", targetColorCustom); + let nodes = graphLines.nodes(); + for (let i = 0; i < nodes.length; i++) { + let g = nodes[i].__data__; + if (g.p.lineWeight) { + d3.select(nodes[i]).style("stroke-width", g.p.lineWeight); + } + if (g.p.dashStyle) { + d3.select(nodes[i]).style("stroke-dasharray", g.p.dashStyle); + } + } if (ifURL && !trigger) addPhonesToUrl(); if (stickyLabels) drawLabels(); } @@ -1267,19 +1296,19 @@ function updatePhoneTable() { let f = c.enter().append("tr"), td = () => f.append("td"); f .call(setHover, h => p => hl(p,h)) - .style("color", p => getDivColor(p.id,true)); + .style("color", p => getDivColor(p.id,true,p.hexColor)); td().attr("class","remove").text("⊗") .on("click", removePhone) .style("background-image",colorBar) - .filter(p=>!p.isTarget).append("svg").call(addColorPicker); + .append("svg").call(addColorPicker); // .filter(p=>!p.isTarget) if you want to exclude target from color picker 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); + .style("background-color",p=>getCurveColor(p.id,0,p.hexColor)) + .call(makeColorPicker); // .filter(p=>!p.isTarget) if you want to exclude target from color picker td().attr("class","channels").append("svg").call(addKey) td().attr("class","levels").append("input") .attrs({type:"number",step:"any",value:0}) @@ -1335,6 +1364,50 @@ function updatePhoneTable() { d:"M265 110V25q0 -10 -10 -10H105q-24 0 -48 20l-24 20q-24 20 -2 40l18 15q24 20 42 20h100" }); }); + if (allowSquigDownload) { + function saveSquiggle(p) { + let channels = p.activeCurves.map(c => c.l); + let LR = p.activeCurves.map(c => c.id.match(/(?<=\().(?=\))/)? c.id.match(/(?<=\().(?=\))/)[0] : "Target"); + + let squig = document.createElement("a"); + squig.style.display = 'none'; + document.body.appendChild(squig); + + // this code is if you want to download the average curve if multiple channels are visible + if (channels.length > 1) { + let squigName = p.fullName + " AVG"; + p.comp ? squigName += " - Comp: " + p.comp : null; + let filename = squigName + ".txt"; + let data = avgCurves(channels).map(d => d.join("\t")).join("\n"); + let blob = new Blob([data], {type: "text/plain; charset=utf-8"}); + let url = URL.createObjectURL(blob); + + squig.href = url; + squig.download = filename; + squig.click(); + URL.revokeObjectURL(url); + } else { + let squigName = p.fullName + " " + LR[0]; + p.comp ? squigName += " - " + p.comp : null; + let filename = squigName + ".txt"; + let data = channels[0].map(d => d.join("\t")).join("\n"); + let blob = new Blob([data], {type: "text/plain; charset=utf-8"}); + let url = URL.createObjectURL(blob); + + squig.href = url; + squig.download = filename; + squig.click(); + URL.revokeObjectURL(url); + } + + // cleanup + document.body.removeChild(squig); + } + + td().attr("class","button button-saveSquig") + .html("") + .on("click", saveSquiggle); + } } function updateChannels(p, ch, comp) { @@ -1447,7 +1520,7 @@ function addKey(s) { 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)] + [o, getCurveColor(p.id, o<0.3?-1:o<0.7?0:1,p.hexColor)] )).join("stop") .attr("offset",i=>i[0]) .attr("stop-color",i=>i[1]); @@ -1460,7 +1533,7 @@ function addKey(s) { 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) + .attr("stroke", p => notMultichannel(p) ? getCurveColor(p.id,0,p.hexColor) : "url(#chgrad"+p.id+")"); t.selectAll().data(p=>p.isTarget?[]:LR) .join("text").attr("class","keyCLabel") @@ -1470,7 +1543,7 @@ function addKey(s) { 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)}) + .attrs({dy:"0.32em", "font-size":8, fill:p=>getCurveColor(p.id,0,p.hexColor)}) .text("Target"); let uchl = f => function (p) { updateCurves(p, f(p)); hl(p,true); @@ -1646,22 +1719,154 @@ function addColorPicker(svg) { svg.call(cpCircles); makeColorPicker(svg); } +function hclToHex(hcl) { + return d3.hcl(hcl).formatHex(); +} +function createPopupMenu(p, x, y) { + // Remove any existing popup + d3.select(".colorStylePicker").remove(); + + // Create the popup menu + let popup = d3.select("body").append("div") + .attr("class", "colorStylePicker") + .style("position", "absolute") + .style("left", `${x}px`) + .style("top", `${y}px`) + .style("padding", "10px") + + + // Add a color wheel to the left side + let colorWheelElement = popup.append("div").attr("class", "left-side").node(); + let colorWheel = new ReinventedColorWheel({ + appendTo: colorWheelElement, + wheelDiameter: 150, + wheelThickness: 10, + handleDiameter: 16, + wheelReflectsSaturation: true, + hex: p.hexColor ? p.hexColor : hclToHex(getCurveColor(p.id,0)), + onChange: function(color) { + p.hexColor = color.hex; + input.node().value = color.hex; + colorPhones(); + } + }) + + // Add a right side menu + let menuRightSide = popup.append("div").attr("class", "right-side"); + + // lmao why button when can div + menuRightSide.append("div") + .style("margin-bottom", "3px") + .style("display", "flex") + .style("flex-direction", "column") + .style("align-items", "flex-end") + .append("span").text("X").style("cursor", "pointer").on("click", function() {popup.remove();}); + + // Add input row for hex code + menuRightSide.append("div").style("margin-bottom", "3px").append("span").text("Custom Graph Color").style("margin-right", "108px"); + let hexMenu = menuRightSide.append("div").attr("class", "row"); + hexMenu.append("span").text("Hex Color"); + let input = hexMenu.append("input") + .attr("type", "text") + .attr("value", p.hexColor ? p.hexColor : hclToHex(getCurveColor(p.id,0))) + .on("input", function() { + if (!this.value.startsWith("#")) { + this.value = "#" + this.value.replace(/#/g, ""); + } + if (this.value.match(/^#[0-9A-Fa-f]{6}$/)) { + colorWheel.hex = this.value; + } + }); + + // Add a button to apply random colors + hexMenu.append("button").text("Random").on("click", function() { + p.id = getPhoneNumber(); + // colorPhones(); + colorWheel.hex = hclToHex(getCurveColor(p.id,0)); + // change the hex color input value + // input.node().value = p.hexColor ? p.hexColor : hclToHex(getCurveColor(p.id,0)); + }); + + // Add a 2nd row for dash-style buttons + menuRightSide.append("div").style("margin", "10px 0 3px 0").append("span").text("Custom Graph Dash-Style"); + let styleMenu = menuRightSide.append("div").attr("class", "row"); + + let tick, space; + function getDashStyle() { + if (!p.dashStyle) { + if (p.isTarget) { + tick = 6; + space = 3; + } else if (p.isPrefBounds) { + tick = 6; + space = 3; + } else { + tick = 1; + space = 0; + } + } else { + let dash = p.dashStyle.split(", "); + tick = dash[0]; + space = dash[1]; + } + } + function resolveDashStyle() { + p.dashStyle = tick + ", " + space; + updatePaths(); + } + getDashStyle(); + + styleMenu.append("span").text("Tick").attr("class", "tickText"); + styleMenu.append("input") + .attr("class", "tickInput") + .attr("type", "number") + .attr("value", tick) + .attr("min", 1) + .on("input", function() { + tick = this.value; + resolveDashStyle(); + }); + styleMenu.append("span").text("Space").style("margin-left", "11px").attr("class", "spaceText"); + styleMenu.append("input") + .attr("class", "spaceInput") + .attr("type", "number") + .attr("value", space) + .attr("min", 0) + .on("input", function() { + space = this.value; + resolveDashStyle(); + }); + + // Add an event listener to close the popup when clicking outside of it + document.addEventListener("click", function(event) { + if (!popup.node().contains(event.target)) { + popup.remove(); + document.removeEventListener("click", arguments.callee); + } + }); +} function makeColorPicker(elt) { elt.on("click", function (p) { - p.id = getPhoneNumber(); - colorPhones(); + // Get the bounding box of the clicked element + let bbox = this.getBoundingClientRect(); + + // Calculate the position for the popup menu + let x = bbox.left + window.scrollX + 20; + let y = bbox.top + window.scrollY - 150; + + createPopupMenu(p, x, y); d3.event.stopPropagation(); }); } function colorPhones() { updatePaths(); - let c = p=>p.active?getDivColor(p.id,true):null; + let c = p=>p.active?getDivColor(p.id,true,p.hexColor):null; doc.select("#phones").selectAll("div") .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.select("button").style("background-color",p=>getCurveColor(p.id,0,p.hexColor)); t= t.call(s => s.select(".remove").style("background-image",colorBar) .select("svg").call(cpCircles)) .select("td.channels"); // Key line @@ -1755,8 +1960,9 @@ function showPhone(p, exclusive, suppressVariant, trigger) { setAddButton(false); } } + let PB = p => p.isPrefBounds ? p.isPrefBounds : false; let keep = !exclusive ? (q=>true) - : (q => q.copyOf===p || q.pin || q.isTarget!==p.isTarget || q.isPrefBounds); + : (q => q.copyOf===p || q.pin || q.isTarget!==p.isTarget || PB(q)); if (!p.rawChannels) { loadFiles(p, function (ch) { if (p.rawChannels) return; @@ -1994,8 +2200,8 @@ d3.json(typeof PHONE_BOOK !== "undefined" ? PHONE_BOOK 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)) + .on("mouseover", bg(true, p => getDivColor(p.id===undefined?nextPhoneNumber():p.id, true,p.hexColor))) + .on("mouseout" , bg(false,p => p.id!==undefined?getDivColor(p.id,p.active,p.hexColor):null)) .call(setClicks(showPhone)); phoneDiv.append("span").text(p=>p.fullName); // Adding the + selection button @@ -2048,6 +2254,9 @@ d3.json(typeof PHONE_BOOK !== "undefined" ? PHONE_BOOK inits.map(p => p.copyOf ? showVariant(p.copyOf, p, initMode) : showPhone(p,0,1, initMode)); + // update y scaling + checkUserDefaultScale(); + // -------------------- Custom DF Tilt -------------------- // let UnTiltTHIS = doc.select("#cusdf-UnTiltTHIS"); // if UnTiltTHIS is clicked, switch df to current active target if exists @@ -2068,9 +2277,9 @@ d3.json(typeof PHONE_BOOK !== "undefined" ? PHONE_BOOK } // Bass Shelf let filters = [ - {disabled: false, type:"LSQ", freq:105, q:0.7759, gain:boost}, - {disabled: false, type:"PK", freq:2800, q:1.8, gain:ear}, - {disabled: false, type:"HSQ", freq:2500, q:0.3913, gain:treble} + {disabled: false, type:"LSQ", freq:105, q:0.707, gain:boost}, + {disabled: false, type:"PK", freq:2750, q:1, gain:ear}, + {disabled: false, type:"HSQ", freq:2500, q:0.42, gain:treble} ]; let bass = df.rawChannels.map(c => c ? Equalizer.apply(c, filters) : null); // Tilt @@ -2080,7 +2289,8 @@ d3.json(typeof PHONE_BOOK !== "undefined" ? PHONE_BOOK if (boost == 0) { gainAdjustment = tilt * Math.log2(bass[0][i][0]); } else { - if (bass[0][i][0] >= 200) gainAdjustment = tilt * Math.log2(bass[0][i][0]/200); + // if (bass[0][i][0] >= 200) gainAdjustment = tilt * Math.log2(bass[0][i][0]/200); // stopping tilt at 200hz for bass boost + gainAdjustment = tilt * Math.log2(bass[0][i][0]); } let tiltedMagnitude = bass[0][i][1] + gainAdjustment; tiltOct[i] = [bass[0][i][0], tiltedMagnitude]; @@ -2088,36 +2298,39 @@ d3.json(typeof PHONE_BOOK !== "undefined" ? PHONE_BOOK // New Tilt let brand = window.brandTarget; let phoneObjs = brand.phoneObjs; - let fullDispName = customTiltName; - tilt != 0 || boost != 0 || treble != 0 || ear != 0 ? fullDispName += " (" : null; - tilt != 0 ? fullDispName += "Tilt: " + tilt + "dB/Oct" : null; - tilt != 0 && (boost != 0 || treble !=0 || ear != 0) ? fullDispName += ", " : null; - boost != 0 ? fullDispName += "Bass: " + boost + "dB" : null; - boost != 0 && (treble != 0 || ear != 0) ? fullDispName += ", " : null; - treble != 0 ? fullDispName += "Treble: " + treble + "dB" : null; - treble != 0 && ear != 0 ? fullDispName += ", " : null; - ear != 0 ? fullDispName += "3kHz: " + ear + "dB" : null; - tilt != 0 || boost != 0 || treble != 0 || ear != 0 ? fullDispName += ")" : null; + let preferenceAdjustments = " "; + tilt != 0 || boost != 0 || treble != 0 || ear != 0 ? preferenceAdjustments += "(" : null; + tilt != 0 ? preferenceAdjustments += "Tilt: " + tilt + "dB/Oct" : null; + tilt != 0 && (boost != 0 || treble !=0 || ear != 0) ? preferenceAdjustments += ", " : null; + boost != 0 ? preferenceAdjustments += "Bass: " + boost + "dB" : null; + boost != 0 && (treble != 0 || ear != 0) ? preferenceAdjustments += ", " : null; + treble != 0 ? preferenceAdjustments += "Treble: " + treble + "dB" : null; + treble != 0 && ear != 0 ? preferenceAdjustments += ", " : null; + ear != 0 ? preferenceAdjustments += "3kHz: " + ear + "dB" : null; + tilt != 0 || boost != 0 || treble != 0 || ear != 0 ? preferenceAdjustments += ")" : null; if (tilt == 0 && boost == 4.8 && treble == -4.4 && ear == 0) { - fullDispName += " (Harman 2013 Filters)" + preferenceAdjustments += " (Harman 2013 Filters)" } else if (tilt == 0 && boost == 6.6 && treble == -1.4 && ear == 0) { - fullDispName += " (Harman 2015 Filters)" - } else if (tilt == 0 && boost == 6.6 && treble == -1.4 && ear == -2) { - fullDispName += " (Harman 2018 Filters)" + preferenceAdjustments += " (Harman 2015 Filters)" + } else if (tilt == 0 && boost == 6.6 && treble == -3 && ear == -1.8) { + preferenceAdjustments += " (Harman 2018 Filters)" } let phoneObj = { isTarget:true, brand:brand, phone:"Custom Tilt", - fullName:fullDispName, - dispName:"Custom " + customTiltName + " Tilt", + fullName:customTiltName + preferenceAdjustments, + dispName:customTiltName + preferenceAdjustments, fileName:"Custom Tilt"}; phoneObj.rawChannels = [tiltOct]; phoneObj.id = -69; - let oldPhoneObj = brand.phoneObjs.filter(p => p.phone == "Custom Tilt")[0] + let oldPhoneObj = phoneObjs.filter(p => p.phone == "Custom Tilt")[0]; if (oldPhoneObj) { + // oldPhoneObj.active && removePhone(oldPhoneObj); phoneObj.id = oldPhoneObj.id; phoneObjs[phoneObjs.indexOf(oldPhoneObj)] = phoneObj; + oldPhoneObj.active = false; // worse, but more aesthetic + activePhones = activePhones.filter(p => p.active); updatePhoneTable(); } else { phoneObjs.push(phoneObj); @@ -2191,8 +2404,8 @@ d3.json(typeof PHONE_BOOK !== "undefined" ? PHONE_BOOK this.classList.add("harman2018"); tilt = 0; boost = 6.6; - treble = -1.4; - ear = -2; + treble = -3; + ear = -1.8; break; case "harman2018": this.classList.remove("harman2018"); @@ -2206,8 +2419,8 @@ d3.json(typeof PHONE_BOOK !== "undefined" ? PHONE_BOOK this.className = "harman2018" tilt = 0; boost = 6.6; - treble = -1.4; - ear = -2; + treble = -3; + ear = -1.8; break; } updateDF(boost, tilt, ear, treble); @@ -3347,7 +3560,7 @@ function addExtra() { nodes[nodes.length - 1].connect(merger, 0, 1); // Connect to the right channel nodes.push(merger); } - + filters.forEach(filterInfo => { const filter = audioContext.createBiquadFilter(); let type; @@ -3369,7 +3582,7 @@ function addExtra() { nodes[nodes.length - 1].connect(channelSplitter); } - + // load pink noise audio file document.addEventListener("DOMContentLoaded", () => { pinkNoiseAudio = document.getElementById("pinkNoiseAudio"); @@ -3381,7 +3594,7 @@ function addExtra() { // apply filters applyFilters(audioContext, currentSource, elemToFilters()); - + // track swapping let pinkNoisePlayButton = document.getElementById("play-button"); let eqDemo = document.querySelector("div.eq-demo"); @@ -3512,7 +3725,8 @@ function addExtra() { // get average of all active headphones except targets function getAvgAll() { - let v = activePhones.filter(p => !p.isTarget).map(p => getAvg(p)); + let PB = p => p.isPrefBounds ? p.isPrefBounds : false; + let v = activePhones.filter(p => !p.isTarget && !PB(p)).map(p => getAvg(p)); return avgCurves(v); } // draw average of all active headphones diff --git a/assets/js/reinvented-color-wheel.min.js b/assets/js/reinvented-color-wheel.min.js new file mode 100644 index 0000000..c6b68b6 --- /dev/null +++ b/assets/js/reinvented-color-wheel.min.js @@ -0,0 +1,43 @@ +var ReinventedColorWheel=function(){"use strict" +var e=function(e){var t=e[0],n=e[1]/100,h=e[2]/100 +return 0===h?[0,0,0]:[t,2*(n*=(h*=2)<=1?h:2-h)/(h+n)*100,(h+n)/2*100]},t=function(e){var t,n,h=e[0],i=e[1]/100,r=e[2]/100 +return t=i*r,[h,100*(t=(t/=(n=(2-i)*r)<=1?n:2-n)||0),100*(n/=2)]} +function n(e){var t=e[0],n=e[1],h=e[2],i=Math.max(t,n,h),r=i-Math.min(t,n,h),s=r&&60*(i===t?(n-h)/r%6:i===n?(h-t)/r+2:(t-n)/r+4) +return[s<0?s+360:s,i&&100*r/i,100*i/255]}function h(e){var t=e[0]/60,n=e[1]/100,h=e[2]/100,i=h*n,r=h-i,s=255*(i*(1-Math.abs(t%2-1))+r)+.5|0,a=255*(i+r)+.5|0,o=255*r+.5|0,l=0|t +return 1===l?[s,a,o]:2===l?[o,a,s]:3===l?[o,s,a]:4===l?[s,o,a]:5===l?[a,o,s]:[a,s,o]}function i(e){var t,n=Math.round((t=e,0,255,Math.min(Math.max(t,0),255))).toString(16) +return 1==n.length?"0"+n:n}var r=function(e){var t=4===e.length?i(255*e[3]):"" +return"#"+i(e[0])+i(e[1])+i(e[2])+t},s=function(e){4!==e.length&&5!==e.length||(e=function(e){for(var t="#",n=1;n100?100:(10*e+.5|0)/10}function c(e){return"number"==typeof e&&isFinite(e)}var v="undefined"!=typeof globalThis?globalThis:self,d="PointerEvent"in v?function(e,t,n){e.addEventListener("pointerdown",(function(e){0===e.button&&!1!==t(e)&&this.setPointerCapture(e.pointerId)})),e.addEventListener("pointermove",(function(e){this.hasPointerCapture(e.pointerId)&&n(e)}))}:"ontouchend"in v?function(e,t,n){var h=!1 +e.addEventListener("touchstart",(function(e){1===e.touches.length&&!1!==t(e.touches[0])&&(h=!0,e.preventDefault())})),e.addEventListener("touchmove",(function(e){h&&1===e.touches.length&&(e.preventDefault(),n(e.touches[0]))}))}:function(e,t,n){var h=function(e){n(e)},i=function(){removeEventListener("mouseup",i),removeEventListener("mousemove",h)} +e.addEventListener("mousedown",(function(e){0===e.button&&!1!==t(e)&&(addEventListener("mousemove",h),addEventListener("mouseup",i))}))},p={hsv:[0,100,100],hsl:[0,100,50],wheelDiameter:200,wheelThickness:20,handleDiameter:16,wheelReflectsSaturation:!0,onChange:function(){}},f=v.DOMMatrix||v.WebKitCSSMatrix||v.MSCSSMatrix,m=function(e,t){return e===t||e[0]===t[0]&&e[1]===t[1]&&e[2]===t[2]} +function g(e,t){var n=document.createElement(e) +return n.className=t,n}return function(){function i(e){var t=this +this.options=e,this.wheelDiameter=this.options.wheelDiameter||p.wheelDiameter,this.wheelThickness=this.options.wheelThickness||p.wheelThickness,this.handleDiameter=this.options.handleDiameter||p.handleDiameter,this.onChange=this.options.onChange||p.onChange,this.wheelReflectsSaturation=void 0!==this.options.wheelReflectsSaturation?this.options.wheelReflectsSaturation:p.wheelReflectsSaturation,this.rootElement=this.options.appendTo.appendChild(g("div","reinvented-color-wheel")),this.hueWheelElement=this.rootElement.appendChild(g("canvas","reinvented-color-wheel--hue-wheel")),this.hueWheelContext=this.hueWheelElement.getContext("2d"),this.hueHandleElement=this.rootElement.appendChild(g("div","reinvented-color-wheel--hue-handle")),this.svSpaceElement=this.rootElement.appendChild(g("canvas","reinvented-color-wheel--sv-space")),this.svSpaceContext=this.svSpaceElement.getContext("2d"),this.svHandleElement=this.rootElement.appendChild(g("div","reinvented-color-wheel--sv-handle")),this._redrawHueWheel=function(){t._redrawHueWheelRequested=!1 +var e=t.wheelDiameter,n=e/2,h=n-t.wheelThickness/2,i=Math.PI/180,r=t.wheelReflectsSaturation?","+t._hsl[1]+"%,"+t._hsl[2]+"%)":",100%,50%)",s=t.hueWheelContext +s.clearRect(0,0,e,e),s.lineWidth=t.wheelThickness +for(var a=0;a<360;a++)s.beginPath(),s.arc(n,n,h,(a-90.7)*i,(a-89.3)*i),s.strokeStyle="hsl("+a+r,s.stroke()},this.hueWheelContext.imageSmoothingEnabled=!1,this.svSpaceContext.imageSmoothingEnabled=!1,this._hsv=a(e.hsv?e.hsv:e.hsl?i.hsl2hsv(e.hsl):e.rgb?i.rgb2hsv(e.rgb):e.hex?i.rgb2hsv(i.hex2rgb(e.hex)):void 0,p.hsv),this._hsl=o(i.hsv2hsl(this._hsv)),this._rgb=i.hsv2rgb(this._hsv),this._hex=i.rgb2hex(this._rgb) +var n=function(e,n){var h=t._inverseTransform.multiply(new f("matrix(1,0,0,1,"+e+","+n+")")) +return{x:h.e,y:h.f}},h=function(e){t._inverseTransform=function(e){for(var t=[e];e=e.parentElement;)t.push(e) +for(var n=new f,h=t.length-1;h>=0;h--){var i=getComputedStyle(t[h]),r=i.transform +if(r&&"none"!==r){var s=i.transformOrigin.split(" ").map(parseFloat) +n=n.translate(s[0],s[1]).multiply(new f(r)).translate(-s[0],-s[1])}}return n.inverse()}(e) +var h=e.getBoundingClientRect() +t._center=n(h.left+h.width/2,h.top+h.height/2)},r=function(e){var h=n(e.clientX,e.clientY),i=h.x-t._center.x,r=h.y-t._center.y,s=Math.atan2(r,i) +t.hsv=[180*s/Math.PI+90,t.hsv[1],t.hsv[2]]},s=function(e){var h=n(e.clientX,e.clientY),i=100/t.svSpaceElement.width,r=(h.x-t._center.x)*i+50,s=(t._center.y-h.y)*i+50 +t.hsv=[t._hsv[0],r,s]},l=function(e){h(t.svSpaceElement),s(e)} +d(this.hueWheelElement,(function(e){h(t.hueWheelElement) +var i=n(e.clientX,e.clientY),s=i.x-t._center.x,a=i.y-t._center.y,o=t.wheelDiameter/2-t.wheelThickness +if(s*s+a*abody { opacity: 0.0; transition: opacity 0.1s ease-in; } + @@ -50,6 +51,7 @@ + diff --git a/index.html b/index.html index f4eab6b..bd5c051 100644 --- a/index.html +++ b/index.html @@ -40,6 +40,7 @@ + @@ -50,6 +51,7 @@ +