Compare commits
	
		
			No commits in common. "6c6220e5db54f2ff6333f2ea3d6d4a2d91d6364b" and "346ad930b500019cb1279cb936ce02ccbc556d7f" have entirely different histories.
		
	
	
		
			6c6220e5db
			...
			346ad930b5
		
	
		
							
								
								
									
										20
									
								
								deno.json
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								deno.json
									
									
									
									
									
								
							@ -1,9 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  "compilerOptions": { "types": ["*.d.ts"], "checkJs": true },
 | 
			
		||||
  "imports": {
 | 
			
		||||
    "@twind/": "https://esm.sh/@twind/",
 | 
			
		||||
    "react": "https://esm.sh/preact@10.11.3/compat",
 | 
			
		||||
    "htm": "https://esm.sh/htm@3.1.1/preact",
 | 
			
		||||
    "app": "./js/app.js"
 | 
			
		||||
  "compilerOptions": { "types": ["./store"], "checkJs": true },
 | 
			
		||||
  "importMap": "./deno.map.json",
 | 
			
		||||
  "fmt": {
 | 
			
		||||
    "options": {
 | 
			
		||||
      "lineWidth": 256
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "tasks": {
 | 
			
		||||
    "dev": "deno task serve & deno task test",
 | 
			
		||||
    "fmt": "deno fmt --watch",
 | 
			
		||||
    "serve": "deno run -A --unstable --no-check https://deno.land/std@0.167.0/http/file_server.ts",
 | 
			
		||||
    "test": "deno test store.test.ts --watch --no-lock --no-check"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								deno.map.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								deno.map.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
  "imports": {
 | 
			
		||||
    "@twind/": "https://esm.sh/@twind/",
 | 
			
		||||
    "react": "https://esm.sh/preact@10.11.3/compat",
 | 
			
		||||
    "htm": "https://esm.sh/htm@3.1.1/preact",
 | 
			
		||||
    "app": "./src/app.js"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								index.html
									
									
									
									
									
								
							@ -1,12 +1,7 @@
 | 
			
		||||
<html>
 | 
			
		||||
    <head>
 | 
			
		||||
        <meta charset="utf-8"/>
 | 
			
		||||
        <meta name="viewport" content="width=device-width, initial-scale=1"/>
 | 
			
		||||
    </head>
 | 
			
		||||
    <body>
 | 
			
		||||
        <div id="app"></div>
 | 
			
		||||
        <script type="importmap-shim" src="./deno.json"></script>
 | 
			
		||||
        <script type="module-shim">import "app";</script>
 | 
			
		||||
        <script async src="https://unpkg.com/es-module-shims@0.13.1/dist/es-module-shims.min.js"></script>
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
<meta charset="utf-8"/>
 | 
			
		||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
 | 
			
		||||
<!---->
 | 
			
		||||
<div id="app"></div>
 | 
			
		||||
<script type="importmap-shim" src="./deno.map.json"></script>
 | 
			
		||||
<script type="module-shim">import "app";</script>
 | 
			
		||||
<script async src="https://unpkg.com/es-module-shims@0.13.1/dist/es-module-shims.min.js"></script>
 | 
			
		||||
							
								
								
									
										383
									
								
								js/ui.js
									
									
									
									
									
								
							
							
						
						
									
										383
									
								
								js/ui.js
									
									
									
									
									
								
							@ -1,383 +0,0 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { html } from "htm";
 | 
			
		||||
import * as Store from "./store.js";
 | 
			
		||||
import * as Tone from "./tone.js";
 | 
			
		||||
 | 
			
		||||
/** @typedef {({children}:{children?:preact.ComponentChildren})=>preact.VNode} BasicElement */
 | 
			
		||||
 | 
			
		||||
/** @type {({children, icon, light, disabled, inactive, onClick, classes}:{children:preact.VNode, icon?:preact.VNode, light:boolean, disabled:boolean, inactive:boolean, onClick:()=>void, classes?:string})=>preact.VNode} */
 | 
			
		||||
export function Button({children, icon, light, disabled, inactive, onClick, classes})
 | 
			
		||||
{
 | 
			
		||||
    const [FlashGet, FlashSet] = React.useState(0);
 | 
			
		||||
    const handleClick =()=>
 | 
			
		||||
    {
 | 
			
		||||
        if(inactive||disabled){ return; }
 | 
			
		||||
        FlashSet(FlashGet+1);
 | 
			
		||||
        onClick();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
    <button
 | 
			
		||||
        onClick=${handleClick}
 | 
			
		||||
        class="relative flex items-stretch rounded-lg text(lg white) border-t(1 solid [#00000011]) border-b(2 solid [#ffffff]) ring-inset ring-black font-sans group transition-all duration-500 ${classes} ${disabled ? "bg-zinc-400" : "bg-earmark"} ${(inactive||disabled) && "cursor-default"}"
 | 
			
		||||
    >
 | 
			
		||||
        <span class="absolute top-0 left-0 w-full h-full rounded-lg bg-black transition-opacity duration-300 opacity-0 ${(!inactive && !disabled) && "group-hover:opacity-50"}"></span>
 | 
			
		||||
        ${ FlashGet > 0 && html`<span key=${FlashGet} class="absolute top-0 left-0 w-full h-full rounded-lg bg-green-400 shadow-glow-green-300 animate-flash"></span>` }
 | 
			
		||||
        
 | 
			
		||||
        ${ icon && html`<span class="flex items-center block relative px-2 border-r(1 [#00000088])">
 | 
			
		||||
            <span class="absolute top-0 left-0 w-full h-full bg-black rounded(tl-lg bl-lg) ${disabled ? "opacity-20" : "opacity-50"}"></span>
 | 
			
		||||
            <span class="relative">${icon}</span>
 | 
			
		||||
        </span>` }
 | 
			
		||||
        <div class="flex-1 flex items-center justify-center text-center px-3 py-2 relative border-l(1 [#ffffff22])">
 | 
			
		||||
            <span class="absolute shadow-glow-yellow-500 top-0 left-1/2 w-6 h-[6px] bg-white rounded-full translate(-x-1/2 -y-1/2) transition-all duration-500 ${light ? "opacity-100" : "opacity-0 scale-y-0"}"></span>
 | 
			
		||||
            ${children}
 | 
			
		||||
        </div>
 | 
			
		||||
    </button>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @type {BasicElement} */
 | 
			
		||||
export const Header =()=>
 | 
			
		||||
{
 | 
			
		||||
    const [State, Dispatch] = Store.Consumer();
 | 
			
		||||
    const grade = Store.Grade(State.Live.Test);
 | 
			
		||||
 | 
			
		||||
    /** @type {(e:Event)=>void} */
 | 
			
		||||
    const handleChange =(e)=> Dispatch({Name:"Test", Data:parseInt(/** @type {HTMLSelectElement}*/(e.target).value)});
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
    <div class="flex flex-row items-stretch bg-metal rounded-lg overflow-hidden shadow-md font-sans">
 | 
			
		||||
 | 
			
		||||
        <div class="p-4">
 | 
			
		||||
            <img class="h-auto max-w-[200px]" src="./logo.png"/>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="p-4 flex-1">
 | 
			
		||||
            <div class="box-buttons w-full">
 | 
			
		||||
                <select id="test-select" class="w-full px-2 py-2 rounded-lg border(1 slate-200) font-bold text(xl white) cursor-pointer bg-earmark" value=${State.TestIndex} onChange=${handleChange}>
 | 
			
		||||
                    ${State.Test.map((t, i)=>html`<option class="text-black" value=${i}>${t.Name}</option>`)}
 | 
			
		||||
                </select>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="box-buttons w-full mt-2">
 | 
			
		||||
                <${Button} inactive=${State.Chan.Value == 0} light=${State.Chan.Value == 0} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Chan", Data:-1})}>None<//>
 | 
			
		||||
                <${Button} inactive=${State.Chan.Value == 1} light=${State.Chan.Value == 1} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Chan", Data:1})}>Slight<//>
 | 
			
		||||
                <${Button} inactive=${State.Chan.Value == 1} light=${State.Chan.Value == 1} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Chan", Data:1})}>Moderate<//>
 | 
			
		||||
                <${Button} inactive=${State.Chan.Value == 1} light=${State.Chan.Value == 1} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Chan", Data:1})}>Severe<//>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="p-4">
 | 
			
		||||
            <div class="box-buttons flex-col w-[200px] h-full justify-center">
 | 
			
		||||
                <div>Complete: ${grade.Done} of ${grade.Total}</div>
 | 
			
		||||
                <div class="w-full h-4 bg-zinc-400 rounded-full overflow-hidden">
 | 
			
		||||
                    <div class="h-full w-[${grade.Done/grade.Total*100}%] bg-earmark"></div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="text-sm">Accuracy: ${grade.Score}%</div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @type {BasicElement} */
 | 
			
		||||
export const Display =()=>
 | 
			
		||||
{
 | 
			
		||||
    const [State, Dispatch] = Store.Consumer();
 | 
			
		||||
    return html`
 | 
			
		||||
    <div class="flex justify-end">
 | 
			
		||||
        <div class="bg-metal rounded-lg overflow-hidden shadow-md p-4">
 | 
			
		||||
            <div class="box-buttons">
 | 
			
		||||
                <${Button} light=${State.Show.Cursor} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"ShowCursor", Data:!State.Show.Cursor})}>Cursor<//>
 | 
			
		||||
                <${Button} light=${State.Show.Answer} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"ShowAnswer", Data:!State.Show.Answer})}>Answer<//>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    `;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** @type {BasicElement} */
 | 
			
		||||
export const Controls =()=>
 | 
			
		||||
{
 | 
			
		||||
    const [State, Dispatch] = Store.Consumer();
 | 
			
		||||
 | 
			
		||||
    const [pulsedGet, pulsedSet] = React.useState(true);
 | 
			
		||||
    const [playGet, playSet] = React.useState(0);
 | 
			
		||||
    React.useEffect(()=>
 | 
			
		||||
    {
 | 
			
		||||
        /** @type {number|undefined} */
 | 
			
		||||
        let timer;
 | 
			
		||||
        if(playGet == 1)
 | 
			
		||||
        {
 | 
			
		||||
            const volNorm = (State.Stim.Value-State.Stim.Min)/(State.Stim.Max-State.Stim.Min);
 | 
			
		||||
            Tone.Play(!pulsedGet, State.Chan.Value, Store.ColumnMapping[State.Freq.Value][0], (volNorm*0.8) + 0.1);
 | 
			
		||||
 | 
			
		||||
            if(State.Live.Freq)
 | 
			
		||||
            {
 | 
			
		||||
                const testMark = State.Live.Freq[/** @type {"TestL"|"TestR"}*/(`Test${State.Chan.Value ? "R":"L"}`)];
 | 
			
		||||
                const handler = testMark.Stim <= State.Stim.Value ? ()=>{playSet(2)} : ()=>{playSet(0)}
 | 
			
		||||
                timer = setTimeout(handler, 800 + Math.random()*1300);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return () => clearTimeout(timer);
 | 
			
		||||
        
 | 
			
		||||
    }, [playGet]);
 | 
			
		||||
 | 
			
		||||
    const classTitle = "flex-1 text-sm"
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
    <div class="flex flex-col w-full md:w-[320px] font-sans justify-center gap-4">
 | 
			
		||||
        <div class="flex-col bg-metal rounded-lg overflow-hidden shadow-md">
 | 
			
		||||
            <div class="p-4 pb-1">
 | 
			
		||||
                <div class="box-buttons min-w-[50%]">
 | 
			
		||||
                    <${Button} inactive=${State.Chan.Value == 0} light=${State.Chan.Value == 0} classes="flex-1" onClick=${()=>Dispatch({Name:"Chan", Data:-1})}>Left Ear<//>
 | 
			
		||||
                    <${Button} inactive=${State.Chan.Value == 1} light=${State.Chan.Value == 1} classes="flex-1" onClick=${()=>Dispatch({Name:"Chan", Data:1})}>Right Ear<//>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="p-4 py-1">
 | 
			
		||||
                <div class="box-buttons min-w-[50%]">
 | 
			
		||||
                    <div class="flex-1 text-center text-shadow-lcd"><strong>${Store.ColumnMapping[State.Freq.Value][0]}</strong> Hz</div>
 | 
			
		||||
                    <${Button} disabled=${State.Freq.Value == State.Freq.Min} onClick=${()=>Dispatch({Name:"Freq", Data:-1})}>
 | 
			
		||||
                        <svg class="my-1 h-3 w-3 overflow-visible stroke(white 2)">
 | 
			
		||||
                            <${Glyph.Minus}/>
 | 
			
		||||
                        </svg>
 | 
			
		||||
                    <//>
 | 
			
		||||
                    <${Button} disabled=${State.Freq.Value == State.Freq.Max} onClick=${()=>Dispatch({Name:"Freq", Data:1})}>
 | 
			
		||||
                        <svg class="my-1 h-3 w-3 overflow-visible stroke(white 2)">
 | 
			
		||||
                            <${Glyph.Plus}/>
 | 
			
		||||
                        </svg>
 | 
			
		||||
                    <//>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="p-4 pt-2">
 | 
			
		||||
                <div class="box-buttons min-w-[50%]">
 | 
			
		||||
                    <div class="flex-1 text-center text-shadow-lcd"><strong>${State.Stim.Value}</strong> dbHL</div>
 | 
			
		||||
                    <${Button} disabled=${State.Stim.Value == State.Stim.Min} onClick=${()=>Dispatch({Name:"Stim", Data:-1})}>
 | 
			
		||||
                    <svg class="my-1 h-3 w-3 overflow-visible stroke(white 2)">
 | 
			
		||||
                            <${Glyph.Minus}/>
 | 
			
		||||
                        </svg>
 | 
			
		||||
                    <//>
 | 
			
		||||
                    <${Button} disabled=${State.Stim.Value == State.Stim.Max} onClick=${()=>Dispatch({Name:"Stim", Data:1})}>
 | 
			
		||||
                        <svg class="my-1 h-3 w-3 overflow-visible stroke(white 2)">
 | 
			
		||||
                            <${Glyph.Plus}/>
 | 
			
		||||
                        </svg>
 | 
			
		||||
                    <//>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="flex-col bg-metal rounded-lg overflow-hidden shadow-md">
 | 
			
		||||
            <div class="p-4 pb-0">
 | 
			
		||||
                <div class="box-buttons flex-1">
 | 
			
		||||
                    <div class="flex-1">
 | 
			
		||||
                        <${Button}
 | 
			
		||||
                            classes="w-full flex-1 self-center"
 | 
			
		||||
                            onClick=${()=>playSet(1)}
 | 
			
		||||
                            disabled=${playGet==1}
 | 
			
		||||
                            icon=${html`<svg class="w-3 h-3 mx-1" viewBox="0 0 20 20">
 | 
			
		||||
                                <polygon points="0,0 20,10 0,20" fill="#ffffff" stroke="none"></polygon>
 | 
			
		||||
                            </svg>`}
 | 
			
		||||
                        >
 | 
			
		||||
                            <span class="py-2">Present Tone</span>
 | 
			
		||||
                        <//>
 | 
			
		||||
                        <div class="flex gap-1 mt-2">
 | 
			
		||||
                            <${Button} onClick=${()=>{pulsedSet(true )}} light=${pulsedGet } inactive${pulsedGet } classes="flex-1 text(center xs)">Pulsed    <//>
 | 
			
		||||
                            <${Button} onClick=${()=>{pulsedSet(false)}} light=${!pulsedGet} inactive${!pulsedGet} classes="flex-1 text(center xs)">Continuous<//>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <svg width="80" height="80" preserveAspectRatio="none" viewBox="0 0 79 79" fill="none" class="mx-auto mt-2">
 | 
			
		||||
                    <circle fill="url(#metal)" cx="39" cy="40" r="35"></circle>
 | 
			
		||||
                    <circle fill="url(#metal)" cx="39.5" cy="39.5" r="29.5" transform="rotate(180 39.5 39.5)"></circle>
 | 
			
		||||
                    <circle fill="url(#metal)" cx="39" cy="40" r="27"></circle>
 | 
			
		||||
                    <circle fill="url(#backwall)" cx="39" cy="40" r="25"></circle>
 | 
			
		||||
                    <ellipse fill="url(#clearcoat)" cx="39" cy="33" rx="20" ry="16"></ellipse>
 | 
			
		||||
                    ${playGet == 2 && html`<circle fill="url(#light)" cx="39.5" cy="39.5" r="36" class="animate-pulse"></circle>`}
 | 
			
		||||
                    <defs>
 | 
			
		||||
                        <linearGradient id="metal" x1="39.5" y1="1" x2="39.5" y2="78" gradientUnits="userSpaceOnUse">
 | 
			
		||||
                            <stop offset="0.0" stop-color="#C4C4C4" stop-opacity="1.0"></stop>
 | 
			
		||||
                            <stop offset="1.0" stop-color="#F2F2F2" stop-opacity="1.0"></stop>
 | 
			
		||||
                        </linearGradient>
 | 
			
		||||
                        <radialGradient id="backwall" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39 56) rotate(-90) scale(45.5 74.4907)">
 | 
			
		||||
                            <stop offset="0.0" stop-color="#AAAAAA" stop-opacity="1.0"></stop>
 | 
			
		||||
                            <stop offset="1.0" stop-color="#333333" stop-opacity="1.0"></stop>
 | 
			
		||||
                        </radialGradient>
 | 
			
		||||
                        <radialGradient id="clearcoat" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39 38.5) rotate(90) scale(50.5 71.9394)">
 | 
			
		||||
                            <stop offset="0.0" stop-color="#ffffff" stop-opacity="0.0"></stop>
 | 
			
		||||
                            <stop offset="0.7" stop-color="#ffffff" stop-opacity="1.0"></stop>
 | 
			
		||||
                        </radialGradient>
 | 
			
		||||
                        <radialGradient id="light" cx="0" cy="0" r="1.0" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.5 39.5) rotate(90) scale(39.5)">
 | 
			
		||||
                            <stop offset="0.2" stop-color="#ffffff" stop-opacity="1.0"></stop>
 | 
			
		||||
                            <stop offset="0.5" stop-color="#ff8800" stop-opacity="1.6"></stop>
 | 
			
		||||
                            <stop offset="0.9" stop-color="#ffffff" stop-opacity="0.0"></stop>
 | 
			
		||||
                        </radialGradient>
 | 
			
		||||
                    </defs>
 | 
			
		||||
                </svg>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="flex-col bg-metal rounded-lg overflow-hidden shadow-md">
 | 
			
		||||
            <div class="p-4">
 | 
			
		||||
                <div class="box-buttons flex-col gap-2 min-w-[50%]">
 | 
			
		||||
                    <${Button}
 | 
			
		||||
                        onClick=${()=>Dispatch({Name:"Mark", Data:true })}
 | 
			
		||||
                        classes="text-md w-full"
 | 
			
		||||
                        icon=${html`
 | 
			
		||||
                            <svg class="h-2 w-2 mx-1 overflow-visible stroke(white 2)">
 | 
			
		||||
                                <${State.Chan.Value ? Glyph.O : Glyph.X}/>
 | 
			
		||||
                            </svg>`}
 | 
			
		||||
                    >
 | 
			
		||||
                        Accept
 | 
			
		||||
                    <//>
 | 
			
		||||
                    <${Button}
 | 
			
		||||
                        onClick=${()=>Dispatch({Name:"Mark", Data:false})}
 | 
			
		||||
                        classes="text-sm w-full"
 | 
			
		||||
                        icon=${html`
 | 
			
		||||
                            <svg class="h-2 w-2 mx-1 overflow-visible stroke(white 2)">
 | 
			
		||||
                                <${State.Chan.Value ? Glyph.O : Glyph.X}>
 | 
			
		||||
                                    <${Glyph.Arrow}/>
 | 
			
		||||
                                <//>
 | 
			
		||||
                            </svg>`}
 | 
			
		||||
                    >
 | 
			
		||||
                        No Response
 | 
			
		||||
                    <//>
 | 
			
		||||
                    <${Button}
 | 
			
		||||
                        icon=${html`
 | 
			
		||||
                        <svg class="h-2 w-2 mx-1 overflow-visible stroke(white 2)">
 | 
			
		||||
                            <${Glyph.Null}/>
 | 
			
		||||
                        </svg>
 | 
			
		||||
                        `}
 | 
			
		||||
                        onClick=${()=>Dispatch({Name:"Mark", Data:null })}
 | 
			
		||||
                        classes="text-sm w-full"
 | 
			
		||||
                        disabled=${State.Live.Mark == undefined}
 | 
			
		||||
                    >
 | 
			
		||||
                        Clear
 | 
			
		||||
                    <//>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    `;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** @type {BasicElement} */
 | 
			
		||||
export const Audiogram =()=>
 | 
			
		||||
{
 | 
			
		||||
    const [State] = Store.Consumer();
 | 
			
		||||
 | 
			
		||||
    const testMarksL = State.Draw.TestL.Points.map(p=>html`<${Mark} x=${p.X} y=${p.Y} response=${p.Mark?.Resp} right=${false}/>`);
 | 
			
		||||
    const userMarksL = State.Draw.UserL.Points.map(p=>html`<${Mark} x=${p.X} y=${p.Y} response=${p.Mark?.Resp} right=${false} classes=${State.Live.Mark == p.Mark ? "stroke-bold":""}/>`);
 | 
			
		||||
    const testMarksR = State.Draw.TestR.Points.map(p=>html`<${Mark} x=${p.X} y=${p.Y} response=${p.Mark?.Resp} right=${true} />`);
 | 
			
		||||
    const userMarksR = State.Draw.UserR.Points.map(p=>html`<${Mark} x=${p.X} y=${p.Y} response=${p.Mark?.Resp} right=${true} classes=${State.Live.Mark == p.Mark ? "stroke-bold":""}/>`);
 | 
			
		||||
 | 
			
		||||
    const testLinesL = State.Draw.TestL.Paths.map( p=>html`<line class="opacity-60" x1=${p.Head.X} y1=${p.Head.Y} x2=${p.Tail.X} y2=${p.Tail.Y} />`);
 | 
			
		||||
    const userLinesL = State.Draw.UserL.Paths.map( p=>html`<line class="opacity-60" x1=${p.Head.X} y1=${p.Head.Y} x2=${p.Tail.X} y2=${p.Tail.Y} />`);
 | 
			
		||||
    const testLinesR = State.Draw.TestR.Paths.map( p=>html`<line class="opacity-60" x1=${p.Head.X} y1=${p.Head.Y} x2=${p.Tail.X} y2=${p.Tail.Y} />`);
 | 
			
		||||
    const userLinesR = State.Draw.UserR.Paths.map( p=>html`<line class="opacity-60" x1=${p.Head.X} y1=${p.Head.Y} x2=${p.Tail.X} y2=${p.Tail.Y} />`);
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
    ${
 | 
			
		||||
        State.Show.Answer && html`
 | 
			
		||||
        <svg class="absolute top-0 w-full h-full overflow-visible stroke(blue-700 bold draw) opacity-50">${testMarksL}${testLinesL}</svg>
 | 
			
		||||
        <svg class="absolute top-0 w-full h-full overflow-visible stroke(red-700 bold draw)  opacity-50">${testMarksR}${testLinesR}</svg>
 | 
			
		||||
        `
 | 
			
		||||
    }
 | 
			
		||||
    <svg class="absolute top-0 w-full h-full overflow-visible stroke(blue-700 2 draw)">${userMarksL}${userLinesL}</svg>
 | 
			
		||||
    <svg class="absolute top-0 w-full h-full overflow-visible stroke(red-700 2 draw)">${userMarksR}${userLinesR}</svg>
 | 
			
		||||
    ${
 | 
			
		||||
        State.Show.Cursor && html`
 | 
			
		||||
        <svg class="absolute top-0 w-1 h-1 overflow-visible transition-all duration-500" style=${{top:State.Draw.Cross?.Y, left:State.Draw.Cross?.X}}>
 | 
			
		||||
            <ellipse cx="0" cy="0" rx="8" ry="30" fill="url(#glow)"></ellipse>
 | 
			
		||||
            <ellipse cx="0" cy="0" rx="30" ry="8" fill="url(#glow)"></ellipse>
 | 
			
		||||
            <defs>
 | 
			
		||||
                <radialGradient id="glow">
 | 
			
		||||
                    <stop stop-color=${State.Chan.Value ? "red" : "blue"} stop-opacity="0.6" offset="0.0"></stop>
 | 
			
		||||
                    <stop stop-color=${State.Chan.Value ? "red" : "blue"} stop-opacity="0.3" offset="0.2"></stop>
 | 
			
		||||
                    <stop stop-color=${State.Chan.Value ? "red" : "blue"} stop-opacity="0.0" offset="1.0"></stop>
 | 
			
		||||
                </radialGradient>
 | 
			
		||||
            </defs>
 | 
			
		||||
        </svg>`
 | 
			
		||||
    }`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/** @type {BasicElement} */
 | 
			
		||||
export function Chart({children})
 | 
			
		||||
{
 | 
			
		||||
    const [State] = Store.Consumer();
 | 
			
		||||
    const inset = 20;
 | 
			
		||||
    /** @type {Array<preact.VNode>} */
 | 
			
		||||
    const rules = [];
 | 
			
		||||
    Store.ColumnMapping.forEach(([label, position, normal])=>
 | 
			
		||||
    {
 | 
			
		||||
        rules.push(html`
 | 
			
		||||
        <span class="block absolute top-[-${inset}px] left-[${position*100}%] w-0 h-[calc(100%+${inset*2}px)] border-r(1 zinc-400) ${!normal && "border-dashed"}">
 | 
			
		||||
            <span class="block absolute top-0 left-0 -translate-x-1/2 -translate-y-full pb-${normal ? 4 : 1}">${label}</span>
 | 
			
		||||
        </span>`
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    for(let db = State.Stim.Min; db <= State.Stim.Max; db+=10)
 | 
			
		||||
    {
 | 
			
		||||
        rules.push(html`
 | 
			
		||||
        <span class="block absolute  left-[-${inset}px]   top-[${((db-State.Stim.Min) / (State.Stim.Max-State.Stim.Min))*100}%]   h-0 w-[calc(100%+${inset*2}px)] border-b(${db == 0 ? "2 black" : "1 zinc-400"})">
 | 
			
		||||
            <span class="block absolute top-0 left-0 -translate-x-full -translate-y-1/2 pr-2">${db}</span>
 | 
			
		||||
        </span>`
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    return html`
 | 
			
		||||
    <div class="relative w-full pb-[calc(55%+60px)] font(sans medium) text(xs) self-start">
 | 
			
		||||
        <div class="absolute right-0 bottom-0 w-[calc(100%-60px)] h-[calc(100%-70px)] border(1 zinc-300)">
 | 
			
		||||
            <span class="block        absolute top-[-65px] left-0  w-full      text(sm center)     font-black">Frequency in Hz</span>
 | 
			
		||||
            <span class="inline-block absolute top-[50%]   left-[-50px] ">
 | 
			
		||||
                <span class="inline-block -rotate-90 origin-top -translate-x-1/2 text(sm center) font-black">
 | 
			
		||||
                    Hearing Level (dbHL)
 | 
			
		||||
                </span>
 | 
			
		||||
            </span>
 | 
			
		||||
            <div class=${`relative top-[${inset}px] left-[${inset}px] w-[calc(100%-${inset*2}px)] h-[calc(100%-${inset*2}px)]`}>
 | 
			
		||||
                <span class="block absolute top-0 left-[-${inset}px] w-[calc(100%+${inset*2}px)] h-[27%] bg-black opacity-5"></span>
 | 
			
		||||
                ${ rules }
 | 
			
		||||
                <div class="absolute top-0 left-0 w-full h-full">
 | 
			
		||||
                    ${ children }
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @type {Record<string, BasicElement>} */
 | 
			
		||||
export const Glyph = {
 | 
			
		||||
    Arrow:()=> html`
 | 
			
		||||
    <line class="stroke-draw" x1="100%" y1="100%" x2="0%"   y2="0%"  ></line>
 | 
			
		||||
    <line class="stroke-draw" x1="100%" y1="100%" x2="25%"  y2="100%"></line>
 | 
			
		||||
    <line class="stroke-draw" x1="100%" y1="100%" x2="100%" y2="25%" ></line>`,
 | 
			
		||||
 | 
			
		||||
    X: ({children})=> html`
 | 
			
		||||
    <line class="stroke-draw" x1="0%" y1="0%"   x2="100%" y2="100%"></line>
 | 
			
		||||
    <line class="stroke-draw" x1="0%" y1="100%" x2="100%" y2="0%"  ></line>
 | 
			
		||||
    <g class="scale-50 translate(x-full y-full) rotate-[-15deg]">${children}</g>`,
 | 
			
		||||
 | 
			
		||||
    O: ({children})=> html`
 | 
			
		||||
    <ellipse class="stroke-draw" cx="50%" cy="50%" rx="60%" ry="60%"></ellipse>
 | 
			
		||||
    <g class="scale-50 rotate-[96deg] translate(-x-[0%] y-full)">${children}</g>`,
 | 
			
		||||
 | 
			
		||||
    Minus:()=>html` <line class="stroke-draw" x1="0%" y1="50%" x2="100%" y2="50%"></line>`,
 | 
			
		||||
    Plus:()=>html`  <line class="stroke-draw" x1="0%" y1="50%" x2="100%" y2="50%"></line>
 | 
			
		||||
                    <line class="stroke-draw" y1="0%" x1="50%" y2="100%" x2="50%"></line>`,
 | 
			
		||||
 | 
			
		||||
    Null:()=>html`
 | 
			
		||||
        <ellipse class="stroke-draw" cx="50%" cy="50%" rx="70%" ry="70%"></ellipse>
 | 
			
		||||
        <line    class="stroke-draw" x1="0%" y1="0%" x2="100%" y2="100%"></line>
 | 
			
		||||
    `
 | 
			
		||||
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** @type {({right, response, x, y, classes}:{right:boolean, response?:boolean, x:number|string, y:number|string, classes:string})=>preact.VNode} */
 | 
			
		||||
export const Mark =({right, response, x, y, classes})=>
 | 
			
		||||
{
 | 
			
		||||
    return html`
 | 
			
		||||
    <svg x=${x} y=${y} width="20" height="20" class="overflow-visible ${classes}">
 | 
			
		||||
        <g class="translate(-x-1/2 -y-1/2)">
 | 
			
		||||
            <${ right ? Glyph.O : Glyph.X }>
 | 
			
		||||
                ${ !response && html`<${Glyph.Arrow}/>` }
 | 
			
		||||
            <//>
 | 
			
		||||
        </g>
 | 
			
		||||
    </svg>`;
 | 
			
		||||
};
 | 
			
		||||
@ -15,19 +15,16 @@ TW.Init(ShadowCSS, ShadowDiv);
 | 
			
		||||
 | 
			
		||||
React.render(html`
 | 
			
		||||
    <${Store.Provider}>
 | 
			
		||||
    <div class="max-w-[1170px] mx-auto">
 | 
			
		||||
        <${UI.Select}/>
 | 
			
		||||
 | 
			
		||||
        <${UI.Header}/>
 | 
			
		||||
 | 
			
		||||
        <div class="flex flex-col items-start lg:flex-row my-4">
 | 
			
		||||
        <div class="flex">
 | 
			
		||||
            <${UI.Controls}/>
 | 
			
		||||
            <${UI.Chart}>
 | 
			
		||||
                <${UI.Audiogram}/>
 | 
			
		||||
            <//>
 | 
			
		||||
            <div class="flex-1">
 | 
			
		||||
                <${UI.Chart}>
 | 
			
		||||
                    <${UI.Audiogram}/>
 | 
			
		||||
                <//>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <${UI.Display}/>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
        
 | 
			
		||||
    <//>
 | 
			
		||||
`, ShadowDiv);
 | 
			
		||||
@ -44,8 +44,7 @@ const Reselect =(inState, inTest)=>
 | 
			
		||||
            if(plot.Hz == hz)
 | 
			
		||||
            {
 | 
			
		||||
                output.Freq = plot;
 | 
			
		||||
                output.Mark = inState.Chan.Value ? plot.UserR : plot.UserL;
 | 
			
		||||
                break;
 | 
			
		||||
                output.Mark = plot[`User${inState.Chan.Value ? "R" : "L"}`];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -132,7 +131,6 @@ export function Reducer(inState, inAction)
 | 
			
		||||
            clone.Live.Mark = Data !== null ? {Stim:clone.Stim.Value, Resp:Data} : undefined;
 | 
			
		||||
            clone.Live.Freq[key] = clone.Live.Mark;
 | 
			
		||||
            clone.Draw[key] = Redraw(clone.Live.Test, clone.Chan.Value, clone.Stim, true);
 | 
			
		||||
            SaveTests(clone);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    else if( Name=="Stim" || Name=="Chan" || Name=="Freq")
 | 
			
		||||
@ -148,102 +146,15 @@ export function Reducer(inState, inAction)
 | 
			
		||||
            clone.Live = Reselect(clone);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    else if (Name == "ShowCursor")
 | 
			
		||||
    {
 | 
			
		||||
        clone.Show.Cursor = Data;
 | 
			
		||||
    }
 | 
			
		||||
    else if (Name == "ShowAnswer")
 | 
			
		||||
    {
 | 
			
		||||
        clone.Show.Answer = Data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return clone;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/** @type {Store.Test[]} */
 | 
			
		||||
const TestDefault = [
 | 
			
		||||
    {
 | 
			
		||||
        Name: "Patient A  Asymmetric Notch",
 | 
			
		||||
        Plot:
 | 
			
		||||
        [
 | 
			
		||||
            { Hz: 500,  TestL: { Stim: 15, Resp: true }, TestR: { Stim: 10, Resp: true } },
 | 
			
		||||
            { Hz: 1000, TestL: { Stim: 10, Resp: true }, TestR: { Stim: 10, Resp: true } },
 | 
			
		||||
            { Hz: 2000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 20, Resp: true } },
 | 
			
		||||
            { Hz: 3000, TestL: { Stim: 30, Resp: true }, TestR: { Stim: 40, Resp: true } },
 | 
			
		||||
            { Hz: 4000, TestL: { Stim: 40, Resp: true }, TestR: { Stim: 55, Resp: true } },
 | 
			
		||||
            { Hz: 6000, TestL: { Stim: 35, Resp: true }, TestR: { Stim: 40, Resp: true } },
 | 
			
		||||
            { Hz: 8000, TestL: { Stim: 20, Resp: true }, TestR: { Stim: 15, Resp: true } }
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        Name: "Patient B High Freq Hearing Loss",
 | 
			
		||||
        Plot:
 | 
			
		||||
        [
 | 
			
		||||
            { Hz: 500,  TestL: { Stim: 10, Resp: true }, TestR: { Stim: 10, Resp: true } },
 | 
			
		||||
            { Hz: 1000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 10, Resp: true } },
 | 
			
		||||
            { Hz: 2000, TestL: { Stim: 10, Resp: true }, TestR: { Stim: 15, Resp: true } },
 | 
			
		||||
            { Hz: 3000, TestL: { Stim: 25, Resp: true }, TestR: { Stim: 20, Resp: true } },
 | 
			
		||||
            { Hz: 4000, TestL: { Stim: 35, Resp: true }, TestR: { Stim: 35, Resp: true } },
 | 
			
		||||
            { Hz: 6000, TestL: { Stim: 50, Resp: true }, TestR: { Stim: 55, Resp: true } },
 | 
			
		||||
            { Hz: 8000, TestL: { Stim: 80, Resp: true }, TestR: { Stim: 75, Resp: true } }
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        Name: "Patient C Unilateral Hearing Loss",
 | 
			
		||||
        Plot:
 | 
			
		||||
        [
 | 
			
		||||
            { Hz: 500,  TestL: { Stim: 15, Resp: true }, TestR: { Stim: 40, Resp: true } },
 | 
			
		||||
            { Hz: 1000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 50, Resp: true } },
 | 
			
		||||
            { Hz: 2000, TestL: { Stim: 20, Resp: true }, TestR: { Stim: 65, Resp: true } },
 | 
			
		||||
            { Hz: 3000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 70, Resp: true } },
 | 
			
		||||
            { Hz: 4000, TestL: { Stim: 20, Resp: true }, TestR: { Stim: 65, Resp: true } },
 | 
			
		||||
            { Hz: 6000, TestL: { Stim: 25, Resp: true }, TestR: { Stim: 60, Resp: true } },
 | 
			
		||||
            { Hz: 8000, TestL: { Stim: 20, Resp: true }, TestR: { Stim: 45, Resp: true } }
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        Name: "Patient D Normal Hearing",
 | 
			
		||||
        Plot:
 | 
			
		||||
        [
 | 
			
		||||
            { Hz: 500,  TestL: { Stim:  5, Resp: true }, TestR: { Stim: 10, Resp: true } },
 | 
			
		||||
            { Hz: 1000, TestL: { Stim:  0, Resp: true }, TestR: { Stim:  5, Resp: true } },
 | 
			
		||||
            { Hz: 2000, TestL: { Stim:  5, Resp: true }, TestR: { Stim:  5, Resp: true } },
 | 
			
		||||
            { Hz: 3000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 10, Resp: true } },
 | 
			
		||||
            { Hz: 4000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 15, Resp: true } },
 | 
			
		||||
            { Hz: 6000, TestL: { Stim:  5, Resp: true }, TestR: { Stim: 10, Resp: true } },
 | 
			
		||||
            { Hz: 8000, TestL: { Stim:  0, Resp: true }, TestR: { Stim:  5, Resp: true } }
 | 
			
		||||
        ]
 | 
			
		||||
    }
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
/** @type {Store.Test[] | string | null } */
 | 
			
		||||
let TestSaved = localStorage.getItem("app-tests");
 | 
			
		||||
if(TestSaved)
 | 
			
		||||
{
 | 
			
		||||
    TestSaved = JSON.parse(TestSaved);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**@type {(inState:Store.State)=>void} */
 | 
			
		||||
const SaveTests =(inState)=> localStorage.setItem("app-tests", JSON.stringify(inState.Test));
 | 
			
		||||
 | 
			
		||||
/**@type {(inState:Store.State)=>void} */
 | 
			
		||||
const SaveSettings =(inState)=> localStorage.setItem("app-settings", JSON.stringify({
 | 
			
		||||
    Chan:inState.Chan,
 | 
			
		||||
    Freq:inState.Freq,
 | 
			
		||||
    Stim:inState.Stim,
 | 
			
		||||
    Show:inState.Show,
 | 
			
		||||
    TestIndex:inState.TestIndex,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/** @type {Store.Test[]} */
 | 
			
		||||
const TestActual = Array.isArray(TestSaved) ? TestSaved : TestDefault
 | 
			
		||||
 | 
			
		||||
/** @type {Store.State} */
 | 
			
		||||
export const Initial = Reducer(
 | 
			
		||||
{
 | 
			
		||||
    Chan: { Min:0,   Max:1,   Value:0,  Step:1 },
 | 
			
		||||
    Freq: { Min:2,   Max:8,   Value:3,  Step:1 },
 | 
			
		||||
    Freq: { Min:2,   Max:8,   Value:2,  Step:1 },
 | 
			
		||||
    Stim: { Min:-10, Max:120, Value:30, Step:5 },
 | 
			
		||||
    Live:
 | 
			
		||||
    {
 | 
			
		||||
@ -258,17 +169,39 @@ export const Initial = Reducer(
 | 
			
		||||
        TestL:{Points:[], Paths:[]},
 | 
			
		||||
        TestR:{Points:[], Paths:[]}
 | 
			
		||||
    },
 | 
			
		||||
    Show:
 | 
			
		||||
    {
 | 
			
		||||
        Cursor:true,
 | 
			
		||||
        Answer:false
 | 
			
		||||
    },
 | 
			
		||||
    TestIndex: 0,
 | 
			
		||||
    Test: TestActual
 | 
			
		||||
    Test: [
 | 
			
		||||
        {
 | 
			
		||||
            Name: "Patient A  Asymmetric Notch",
 | 
			
		||||
            Plot:
 | 
			
		||||
            [
 | 
			
		||||
                { Hz: 500,  TestL: { Stim: 30, Resp: true }, TestR: { Stim: 50, Resp: true } },
 | 
			
		||||
                { Hz: 1000, TestL: { Stim: 50, Resp: true }, TestR: { Stim: 55, Resp: true } },
 | 
			
		||||
                { Hz: 2000, TestL: { Stim: 50, Resp: true }, TestR: { Stim: 55, Resp: true } },
 | 
			
		||||
                { Hz: 3000, TestL: { Stim: 50, Resp: true }, TestR: { Stim: 55, Resp: true } },
 | 
			
		||||
                { Hz: 4000, TestL: { Stim: 50, Resp: true }, TestR: { Stim: 55, Resp: true } },
 | 
			
		||||
                { Hz: 6000, TestL: { Stim: 50, Resp: true }, TestR: { Stim: 55, Resp: true } },
 | 
			
		||||
                { Hz: 8000, TestL: { Stim: 50, Resp: true }, TestR: { Stim: 55, Resp: true } }
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            Name: "Patient B  Asymmetric Notch",
 | 
			
		||||
            Plot:
 | 
			
		||||
            [
 | 
			
		||||
                { Hz: 500,  TestL: { Stim: 50, Resp: true }, TestR: { Stim: 70, Resp: true } },
 | 
			
		||||
                { Hz: 1000, TestL: { Stim: 30, Resp: true }, TestR: { Stim: 25, Resp: true } },
 | 
			
		||||
                { Hz: 2000, TestL: { Stim: 30, Resp: true }, TestR: { Stim: 25, Resp: true } },
 | 
			
		||||
                { Hz: 3000, TestL: { Stim: 30, Resp: true }, TestR: { Stim: 25, Resp: true } },
 | 
			
		||||
                { Hz: 4000, TestL: { Stim: 30, Resp: true }, TestR: { Stim: 25, Resp: true } },
 | 
			
		||||
                { Hz: 6000, TestL: { Stim: 30, Resp: true }, TestR: { Stim: 25, Resp: true } },
 | 
			
		||||
                { Hz: 8000, TestL: { Stim: 30, Resp: true }, TestR: { Stim: 25, Resp: true } }
 | 
			
		||||
            ]
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
}, {Name:"Test", Data:0});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export const Context = React.createContext(/** @type {Store.Binding} */([Initial, (_a)=>{}]));
 | 
			
		||||
/** @type {preact.Context<Store.Binding>} */
 | 
			
		||||
export const Context = React.createContext([Initial, (_a)=>{}]);
 | 
			
		||||
 | 
			
		||||
/** @type {(props:{children:preact.ComponentChildren})=>preact.VNode} */
 | 
			
		||||
export const Provider =(props)=>
 | 
			
		||||
@ -2,7 +2,7 @@ import * as TW   from "@twind/core@1.0.1";
 | 
			
		||||
import TWPreTail from "@twind/preset-tailwind@1.0.1";
 | 
			
		||||
import TWPreAuto from "@twind/preset-autoprefix@1.0.1";
 | 
			
		||||
 | 
			
		||||
/** @type {TW.TwindUserConfig} */
 | 
			
		||||
/** @type {TW.TwindConfig} */
 | 
			
		||||
export const Configure = {
 | 
			
		||||
    theme:
 | 
			
		||||
    {
 | 
			
		||||
@ -51,11 +51,8 @@ export const Configure = {
 | 
			
		||||
        [
 | 
			
		||||
            "bg-metal",
 | 
			
		||||
            {
 | 
			
		||||
                "background": "linear-gradient(159deg, rgb(228, 228, 228) 0%, rgb(243, 243, 243) 25%, rgb(236, 236, 236) 100%)"
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
            "bg-earmark", "bg-gradient-to-b from-[#107c79] to-[#115e67]"
 | 
			
		||||
                "background": "linear-gradient(159deg, rgb(236, 236, 236) 0%, rgb(233, 233, 233) 36%, rgb(213, 213, 213) 36.1%, rgb(236, 236, 236) 100%)"
 | 
			
		||||
            }
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
            'shadow-glow-(.*)',
 | 
			
		||||
@ -70,11 +67,8 @@ export const Configure = {
 | 
			
		||||
                "box-shadow": "rgb(0 0 0 / 50%) 0px -2px 3px inset, rgb(255 255 255 / 50%) 0px 10px 10px inset"
 | 
			
		||||
            }
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
            'text-shadow-lcd', {"text-shadow": "0px 1px 1px #00000055"}
 | 
			
		||||
        ],
 | 
			
		||||
        [ 'box-notch', "border-t(1 [#ffffff]) border-r(1 [#ffffff]) border-b(1 [#00000033]) border-l(1 [#00000033]) flex items-center justify-end gap-1 p-2" ],
 | 
			
		||||
        [ "box-buttons", "flex gap-1 items-center p-2 rounded-lg bg-gradient-to-b from-[#00000022] border-b(1 [#ffffff]) border-t(1 [#00000033])"]
 | 
			
		||||
        [ 'box-notch', "border-t(1 [#ffffff]) border-b(1 [#00000033]) flex items-center justify-end gap-1 p-2" ],
 | 
			
		||||
        [ "box-buttons", "flex gap-1 items-center p-2 rounded-lg bg-gradient-to-b from-zinc-300 border-b(1 [#ffffff]) border-t(1 [#00000033])"]
 | 
			
		||||
    ],
 | 
			
		||||
    presets: [TWPreTail(), TWPreAuto()]
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										300
									
								
								src/ui.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										300
									
								
								src/ui.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,300 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { html } from "htm";
 | 
			
		||||
import * as Store from "./store.js";
 | 
			
		||||
import * as Tone from "./tone.js";
 | 
			
		||||
 | 
			
		||||
/** @typedef {({children}:{children?:preact.ComponentChildren})=>preact.VNode} BasicElement */
 | 
			
		||||
 | 
			
		||||
/** @type {({children, icon, light, disabled, inactive, onClick, classes}:{children:preact.VNode, icon?:preact.VNode, light:boolean, disabled:boolean, inactive:boolean, onClick:()=>void, classes?:string})=>preact.VNode} */
 | 
			
		||||
export function Button({children, icon, light, disabled, inactive, onClick, classes})
 | 
			
		||||
{
 | 
			
		||||
    const [FlashGet, FlashSet] = React.useState(0);
 | 
			
		||||
    const handleClick =()=>
 | 
			
		||||
    {
 | 
			
		||||
        if(inactive||disabled){ return; }
 | 
			
		||||
        FlashSet(FlashGet+1);
 | 
			
		||||
        onClick();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
    <button
 | 
			
		||||
        onClick=${handleClick}
 | 
			
		||||
        class="relative flex items-stretch rounded-lg text(lg white) border-t(1 solid [#00000011]) border-b(2 solid [#ffffff]) ring-inset ring-black font-sans group transition-all ${classes} ${disabled ? "bg-zinc-400" : "bg-gradient-to-b from-emerald-700 to-emerald-500"} ${(inactive||disabled) && "cursor-default"}"
 | 
			
		||||
    >
 | 
			
		||||
        <span class="absolute top-0 left-0 w-full h-full rounded-lg bg-black transition-opacity duration-300 opacity-0 ${(!inactive && !disabled) && "group-hover:opacity-50"}"></span>
 | 
			
		||||
        ${ FlashGet > 0 && html`<span key=${FlashGet} class="absolute top-0 left-0 w-full h-full rounded-lg bg-green-400 shadow-glow-green-300 animate-flash"></span>` }
 | 
			
		||||
        
 | 
			
		||||
        ${ icon && html`<span class="flex items-center block relative px-2 border-r(1 [#00000066])">
 | 
			
		||||
            <span class="absolute top-0 left-0 w-full h-full bg-black rounded(tl-lg bl-lg) ${disabled ? "opacity-20" : "opacity-50"}"></span>
 | 
			
		||||
            <span class="relative">${icon}</span>
 | 
			
		||||
        </span>` }
 | 
			
		||||
        <div class="flex-1 flex items-center justify-center text-center px-3 py-2 relative border-l(1 [#ffffff44])">
 | 
			
		||||
            <span class="absolute shadow-glow-yellow-500 top-0 left-1/2 w-6 h-[6px] bg-white rounded-full translate(-x-1/2 -y-1/2) transition-all duration-500 ${light ? "opacity-100" : "opacity-0 scale-y-0"}"></span>
 | 
			
		||||
            ${children}
 | 
			
		||||
        </div>
 | 
			
		||||
    </button>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @type {BasicElement} */
 | 
			
		||||
export const Select =()=>
 | 
			
		||||
{
 | 
			
		||||
    const [State, Dispatch] = Store.Consumer();
 | 
			
		||||
    const grade = Store.Grade(State.Live.Test);
 | 
			
		||||
 | 
			
		||||
    /** @type {(e:Event)=>void} */
 | 
			
		||||
    const handleChange =(e)=> Dispatch({Name:"Test", Data:parseInt(/** @type {HTMLSelectElement}*/(e.target).value)});
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
    <div class="font-sans">
 | 
			
		||||
        <label for="test-select" class="inline-block">Select Test:</label>
 | 
			
		||||
        <select id="test-select" class="px-2 py-2 rounded border(1 slate-200) inline-block" value=${State.TestIndex} onChange=${handleChange}>
 | 
			
		||||
            ${State.Test.map((t, i)=>html`<option value=${i}>${t.Name}</option>`)}
 | 
			
		||||
        </select>
 | 
			
		||||
        <div>Complete: ${grade.Done} of ${grade.Total}</div>
 | 
			
		||||
        <div>Accuracy: ${grade.Score}%</div>
 | 
			
		||||
        <div class="h-4 bg-gray-200 rounded-full overflow-hidden">
 | 
			
		||||
            <div class="h-full w-[${grade.Done/grade.Total*100}%] bg-emerald-500 shadow-sss"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @type {BasicElement} */
 | 
			
		||||
export const Controls =()=>
 | 
			
		||||
{
 | 
			
		||||
    const [State, Dispatch] = Store.Consumer();
 | 
			
		||||
 | 
			
		||||
    const [pulsedGet, pulsedSet] = React.useState(true);
 | 
			
		||||
    const [playGet, playSet] = React.useState(0);
 | 
			
		||||
    React.useEffect(()=>
 | 
			
		||||
    {
 | 
			
		||||
        /** @type {number|undefined} */
 | 
			
		||||
        let timer;
 | 
			
		||||
        if(playGet == 1)
 | 
			
		||||
        {
 | 
			
		||||
            const volNorm = (State.Stim.Value-State.Stim.Min)/(State.Stim.Max-State.Stim.Min);
 | 
			
		||||
            Tone.Play(!pulsedGet, State.Chan.Value, Store.ColumnMapping[State.Freq.Value][0], (volNorm*0.8) + 0.1);
 | 
			
		||||
 | 
			
		||||
            if(State.Live.Freq)
 | 
			
		||||
            {
 | 
			
		||||
                const testMark = State.Live.Freq[/** @type {"TestL"|"TestR"}*/(`Test${State.Chan.Value ? "R":"L"}`)];
 | 
			
		||||
                const handler = testMark.Stim <= State.Stim.Value ? ()=>{playSet(2)} : ()=>{playSet(0)}
 | 
			
		||||
                timer = setTimeout(handler, 500 + Math.random()*1000);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return () => clearTimeout(timer);
 | 
			
		||||
        
 | 
			
		||||
    }, [playGet]);
 | 
			
		||||
 | 
			
		||||
    const classTitle = "flex-1 text-sm"
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
    <div class="self-start font-sans bg-metal rounded-lg overflow-hidden">
 | 
			
		||||
        <div class="box-notch">
 | 
			
		||||
            <div class=${classTitle}>Channel</div>
 | 
			
		||||
            <div class="box-buttons">
 | 
			
		||||
                <${Button} inactive=${State.Chan.Value == 0} light=${State.Chan.Value == 0} classes="" onClick=${()=>Dispatch({Name:"Chan", Data:-1})}>Left<//>
 | 
			
		||||
                <${Button} inactive=${State.Chan.Value == 1} light=${State.Chan.Value == 1} classes="" onClick=${()=>Dispatch({Name:"Chan", Data:1})}>Right<//>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="box-notch">
 | 
			
		||||
            <div class=${classTitle}>Frequency</div>
 | 
			
		||||
            <div class="box-buttons">
 | 
			
		||||
                <div class="w-24 text-center"><strong>${Store.ColumnMapping[State.Freq.Value][0]}</strong> Hz</div>
 | 
			
		||||
                <${Button} disabled=${State.Freq.Value == State.Freq.Min} onClick=${()=>Dispatch({Name:"Freq", Data:-1})}>-<//>
 | 
			
		||||
                <${Button} disabled=${State.Freq.Value == State.Freq.Max} onClick=${()=>Dispatch({Name:"Freq", Data:1})}>+<//>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="box-notch">
 | 
			
		||||
            <div class=${classTitle}>Stimulus</div>
 | 
			
		||||
            <div class="box-buttons">
 | 
			
		||||
                <div class="w-24 text-center"><strong>${State.Stim.Value}</strong> dbHL</div>
 | 
			
		||||
                <${Button} disabled=${State.Stim.Value == State.Stim.Min} onClick=${()=>Dispatch({Name:"Stim", Data:-1})}>-<//>
 | 
			
		||||
                <${Button} disabled=${State.Stim.Value == State.Stim.Max} onClick=${()=>Dispatch({Name:"Stim", Data:1})}>+<//>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="box-notch">
 | 
			
		||||
            <svg width="80" height="80" preserveAspectRatio="none" viewBox="0 0 79 79" fill="none">
 | 
			
		||||
                <circle fill="url(#metal)" cx="39" cy="40" r="35"></circle>
 | 
			
		||||
                <circle fill="url(#metal)" cx="39.5" cy="39.5" r="29.5" transform="rotate(180 39.5 39.5)"></circle>
 | 
			
		||||
                <circle fill="url(#metal)" cx="39" cy="40" r="27"></circle>
 | 
			
		||||
                <circle fill="url(#backwall)" cx="39" cy="40" r="25"></circle>
 | 
			
		||||
                <ellipse fill="url(#clearcoat)" cx="39" cy="33" rx="20" ry="16"></ellipse>
 | 
			
		||||
                ${playGet == 2 && html`<circle fill="url(#light)" cx="39.5" cy="39.5" r="36" class="animate-pulse"></circle>`}
 | 
			
		||||
                <defs>
 | 
			
		||||
                    <linearGradient id="metal" x1="39.5" y1="1" x2="39.5" y2="78" gradientUnits="userSpaceOnUse">
 | 
			
		||||
                        <stop offset="0.0" stop-color="#C4C4C4" stop-opacity="1.0"></stop>
 | 
			
		||||
                        <stop offset="1.0" stop-color="#F2F2F2" stop-opacity="1.0"></stop>
 | 
			
		||||
                    </linearGradient>
 | 
			
		||||
                    <radialGradient id="backwall" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39 56) rotate(-90) scale(45.5 74.4907)">
 | 
			
		||||
                        <stop offset="0.0" stop-color="#AAAAAA" stop-opacity="1.0"></stop>
 | 
			
		||||
                        <stop offset="1.0" stop-color="#333333" stop-opacity="1.0"></stop>
 | 
			
		||||
                    </radialGradient>
 | 
			
		||||
                    <radialGradient id="clearcoat" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39 38.5) rotate(90) scale(50.5 71.9394)">
 | 
			
		||||
                        <stop offset="0.0" stop-color="#ffffff" stop-opacity="0.0"></stop>
 | 
			
		||||
                        <stop offset="0.7" stop-color="#ffffff" stop-opacity="1.0"></stop>
 | 
			
		||||
                    </radialGradient>
 | 
			
		||||
                    <radialGradient id="light" cx="0" cy="0" r="1.0" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.5 39.5) rotate(90) scale(39.5)">
 | 
			
		||||
                        <stop offset="0.2" stop-color="#ffffff" stop-opacity="1.0"></stop>
 | 
			
		||||
                        <stop offset="0.5" stop-color="#ff8800" stop-opacity="1.6"></stop>
 | 
			
		||||
                        <stop offset="0.9" stop-color="#ffffff" stop-opacity="0.0"></stop>
 | 
			
		||||
                    </radialGradient>
 | 
			
		||||
                </defs>
 | 
			
		||||
            </svg>
 | 
			
		||||
            <div class="box-buttons flex-1">
 | 
			
		||||
                <div class="flex-1">
 | 
			
		||||
                    <${Button}
 | 
			
		||||
                        classes="w-full flex-1 self-center"
 | 
			
		||||
                        onClick=${()=>playSet(1)}
 | 
			
		||||
                        disabled=${playGet==1}
 | 
			
		||||
                        icon=${html`<svg class="w-3 h-3 my-4"><polygon points="0,0 10,5 0,10" fill="#ffffff" stroke="none"></polygon></svg>`}
 | 
			
		||||
                    >
 | 
			
		||||
                        Present Tone
 | 
			
		||||
                    <//>
 | 
			
		||||
                    <div class="flex gap-1 mt-2">
 | 
			
		||||
                        <${Button} onClick=${()=>{pulsedSet(true )}} light=${pulsedGet } inactive${pulsedGet } classes="flex-1 text(center xs)">Pulsed    <//>
 | 
			
		||||
                        <${Button} onClick=${()=>{pulsedSet(false)}} light=${!pulsedGet} inactive${!pulsedGet} classes="flex-1 text(center xs)">Continuous<//>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="box-notch">
 | 
			
		||||
            <div class=${classTitle}>Threshold</div>
 | 
			
		||||
            <div class="box-buttons flex-col gap-2">
 | 
			
		||||
                <${Button}
 | 
			
		||||
                    onClick=${()=>Dispatch({Name:"Mark", Data:true })}
 | 
			
		||||
                    classes="text-md w-full"
 | 
			
		||||
                    icon=${html`<${Mark} right=${State.Chan.Value} response=${true}  x="0" y="0" classes="stroke(white 2 draw) w-2 h-2 translate-x-1/2 translate-y-1/2"/>`}
 | 
			
		||||
                >
 | 
			
		||||
                    Accept
 | 
			
		||||
                <//>
 | 
			
		||||
                <${Button}
 | 
			
		||||
                    onClick=${()=>Dispatch({Name:"Mark", Data:false})}
 | 
			
		||||
                    classes="text-sm"
 | 
			
		||||
                    icon=${html`<${Mark} right=${State.Chan.Value} response=${false} x="0" y="0" classes="stroke(white 2 draw) w-2 h-2 translate-x-1/2 translate-y-1/2"/>`}
 | 
			
		||||
                >
 | 
			
		||||
                    No Response
 | 
			
		||||
                <//>
 | 
			
		||||
                <${Button}
 | 
			
		||||
                    icon=${html`
 | 
			
		||||
                    <svg x="0" y="0" class="translate-x-1/2 translate-y-1/2 stroke-draw h-2 overflow-visible stroke-white w-2 stroke-2">
 | 
			
		||||
                        <ellipse vector-effect="non-scaling-stroke" rx="70%" ry="70%"></ellipse>
 | 
			
		||||
                        <line    vector-effect="non-scaling-stroke" x1="-50%" y1="-50%" x2="50%" y2="50%"></line>
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    `}
 | 
			
		||||
                    onClick=${()=>Dispatch({Name:"Mark", Data:null })}
 | 
			
		||||
                    classes="text-sm w-full"
 | 
			
		||||
                    disabled=${State.Live.Mark == undefined}
 | 
			
		||||
                >
 | 
			
		||||
                    Clear
 | 
			
		||||
                <//>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    `;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** @type {BasicElement} */
 | 
			
		||||
export const Audiogram =()=>
 | 
			
		||||
{
 | 
			
		||||
    const [State] = Store.Consumer();
 | 
			
		||||
 | 
			
		||||
    const testMarksL = State.Draw.TestL.Points.map(p=>html`<${Mark} x=${p.X} y=${p.Y} response=${p.Mark?.Resp} right=${false}/>`);
 | 
			
		||||
    const userMarksL = State.Draw.UserL.Points.map(p=>html`<${Mark} x=${p.X} y=${p.Y} response=${p.Mark?.Resp} right=${false} classes=${State.Live.Mark == p.Mark ? "stroke-bold":""}/>`);
 | 
			
		||||
    const testMarksR = State.Draw.TestR.Points.map(p=>html`<${Mark} x=${p.X} y=${p.Y} response=${p.Mark?.Resp} right=${true} />`);
 | 
			
		||||
    const userMarksR = State.Draw.UserR.Points.map(p=>html`<${Mark} x=${p.X} y=${p.Y} response=${p.Mark?.Resp} right=${true} classes=${State.Live.Mark == p.Mark ? "stroke-bold":""}/>`);
 | 
			
		||||
 | 
			
		||||
    const testLinesL = State.Draw.TestL.Paths.map( p=>html`<line class="opacity-60" x1=${p.Head.X} y1=${p.Head.Y} x2=${p.Tail.X} y2=${p.Tail.Y} />`);
 | 
			
		||||
    const userLinesL = State.Draw.UserL.Paths.map( p=>html`<line class="opacity-60" x1=${p.Head.X} y1=${p.Head.Y} x2=${p.Tail.X} y2=${p.Tail.Y} />`);
 | 
			
		||||
    const testLinesR = State.Draw.TestR.Paths.map( p=>html`<line class="opacity-60" x1=${p.Head.X} y1=${p.Head.Y} x2=${p.Tail.X} y2=${p.Tail.Y} />`);
 | 
			
		||||
    const userLinesR = State.Draw.UserR.Paths.map( p=>html`<line class="opacity-60" x1=${p.Head.X} y1=${p.Head.Y} x2=${p.Tail.X} y2=${p.Tail.Y} />`);
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
    <svg class="absolute top-0 w-full h-full overflow-visible stroke(blue-700 bold draw) opacity-50">${testMarksL}${testLinesL}</svg>
 | 
			
		||||
    <svg class="absolute top-0 w-full h-full overflow-visible stroke(red-700 bold draw)  opacity-50">${testMarksR}${testLinesR}</svg>
 | 
			
		||||
    <svg class="absolute top-0 w-full h-full overflow-visible stroke(blue-700 2 draw)">${userMarksL}${userLinesL}</svg>
 | 
			
		||||
    <svg class="absolute top-0 w-full h-full overflow-visible stroke(red-700 2 draw)">${userMarksR}${userLinesR}</svg>
 | 
			
		||||
    <svg class="absolute top-0 w-1 h-1 overflow-visible transition-all duration-500" style=${{top:State.Draw.Cross?.Y, left:State.Draw.Cross?.X}}>
 | 
			
		||||
        <ellipse cx="0" cy="0" rx="8" ry="30" fill="url(#glow)"></ellipse>
 | 
			
		||||
        <ellipse cx="0" cy="0" rx="30" ry="8" fill="url(#glow)"></ellipse>
 | 
			
		||||
        <defs>
 | 
			
		||||
            <radialGradient id="glow">
 | 
			
		||||
                <stop stop-color=${State.Chan.Value ? "red" : "blue"} stop-opacity="0.6" offset="0.0"></stop>
 | 
			
		||||
                <stop stop-color=${State.Chan.Value ? "red" : "blue"} stop-opacity="0.3" offset="0.2"></stop>
 | 
			
		||||
                <stop stop-color=${State.Chan.Value ? "red" : "blue"} stop-opacity="0.0" offset="1.0"></stop>
 | 
			
		||||
            </radialGradient>
 | 
			
		||||
        </defs>
 | 
			
		||||
    </svg>
 | 
			
		||||
    `;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** @type {BasicElement} */
 | 
			
		||||
export function Chart({children})
 | 
			
		||||
{
 | 
			
		||||
    const [State] = Store.Consumer();
 | 
			
		||||
    const inset = 20;
 | 
			
		||||
    /** @type {Array<preact.VNode>} */
 | 
			
		||||
    const rules = [];
 | 
			
		||||
    Store.ColumnMapping.forEach(([label, position, normal])=>
 | 
			
		||||
    {
 | 
			
		||||
        rules.push(html`
 | 
			
		||||
        <span class="block absolute top-[-${inset}px] left-[${position*100}%] w-0 h-[calc(100%+${inset*2}px)] border-r(1 slate-400) ${!normal && "border-dashed"}">
 | 
			
		||||
            <span class="block absolute top-0 left-0 -translate-x-1/2 -translate-y-full pb-${normal ? 4 : 1}">${label}</span>
 | 
			
		||||
        </span>`
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    for(let db = State.Stim.Min; db <= State.Stim.Max; db+=10)
 | 
			
		||||
    {
 | 
			
		||||
        rules.push(html`
 | 
			
		||||
        <span class="block absolute  left-[-${inset}px]   top-[${((db-State.Stim.Min) / (State.Stim.Max-State.Stim.Min))*100}%]   h-0 w-[calc(100%+${inset*2}px)] border-b(${db == 0 ? "2 black" : "1 slate-400"})">
 | 
			
		||||
            <span class="block absolute top-0 left-0 -translate-x-full -translate-y-1/2 pr-2">${db}</span>
 | 
			
		||||
        </span>`
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    return html`
 | 
			
		||||
    <div class="relative w-full h-[800px] font(sans medium) text(xs)">
 | 
			
		||||
        <div class="absolute right-0 bottom-0 w-[calc(100%-100px)] h-[calc(100%-100px)] border(1 slate-300)">
 | 
			
		||||
            <span class="block        absolute top-[-65px] left-0  w-full      text(sm center)     font-black">Frequency in Hz</span>
 | 
			
		||||
            <span class="inline-block absolute top-[50%]   left-[-65px] ">
 | 
			
		||||
                <span class="inline-block -rotate-90 origin-top -translate-x-1/2 text(sm center) font-black">
 | 
			
		||||
                    Hearing Level (dbHL)
 | 
			
		||||
                </span>
 | 
			
		||||
            </span>
 | 
			
		||||
            <div class=${`relative top-[${inset}px] left-[${inset}px] w-[calc(100%-${inset*2}px)] h-[calc(100%-${inset*2}px)]`}>
 | 
			
		||||
                <span class="block absolute top-0 left-[-${inset}px] w-[calc(100%+${inset*2}px)] h-[27%] bg-black opacity-10"></span>
 | 
			
		||||
                ${ rules }
 | 
			
		||||
                <div class="absolute top-0 left-0 w-full h-full">
 | 
			
		||||
                    ${ children }
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @type {Record<string, BasicElement>} */
 | 
			
		||||
const Glyph = {
 | 
			
		||||
    Arrow:()=> html`
 | 
			
		||||
    <line vector-effect="non-scaling-stroke" x1="100%" y1="100%" x2="0%"   y2="0%"  ></line>
 | 
			
		||||
    <line vector-effect="non-scaling-stroke" x1="100%" y1="100%" x2="25%"  y2="100%"></line>
 | 
			
		||||
    <line vector-effect="non-scaling-stroke" x1="100%" y1="100%" x2="100%" y2="25%" ></line>`,
 | 
			
		||||
 | 
			
		||||
    X: ({children})=> html`
 | 
			
		||||
    <line x1="-50%" y1="-50%" x2="50%" y2="50%" ></line>
 | 
			
		||||
    <line x1="-50%" y1="50%"  x2="50%" y2="-50%"></line>
 | 
			
		||||
    <g class="scale-50 translate(x-1/2 y-1/2) rotate-[-15deg]">${children}</g>`,
 | 
			
		||||
 | 
			
		||||
    O: ({children})=> html`
 | 
			
		||||
    <ellipse vector-effect="non-scaling-stroke" rx="60%" ry="60%"></ellipse>
 | 
			
		||||
    <g style="transform: translate(-40%, 40%) rotate(96deg) scale(0.5);">${children}</g>`
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** @type {({right, response, x, y, classes}:{right:boolean, response?:boolean, x:number|string, y:number|string, classes:string})=>preact.VNode} */
 | 
			
		||||
export const Mark =({right, response, x, y, classes})=>
 | 
			
		||||
{
 | 
			
		||||
    return html`
 | 
			
		||||
    <svg x=${x} y=${y} width="20" height="20" class=${`overflow-visible ${classes}`}>
 | 
			
		||||
        <${ right ? Glyph.O : Glyph.X }>
 | 
			
		||||
            ${ !response && html`<${Glyph.Arrow}/>` }
 | 
			
		||||
        <//>
 | 
			
		||||
    </svg>`;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										5
									
								
								ts/store.d.ts → store.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								ts/store.d.ts → store.d.ts
									
									
									
									
										vendored
									
									
								
							@ -26,7 +26,6 @@ declare namespace Store {
 | 
			
		||||
    Stim: Range;
 | 
			
		||||
    Live: Context;
 | 
			
		||||
    Draw: DrawChart;
 | 
			
		||||
    Show: {Cursor:boolean, Answer:boolean}
 | 
			
		||||
    TestIndex: number;
 | 
			
		||||
    Test: Array<Test>;
 | 
			
		||||
  };
 | 
			
		||||
@ -36,9 +35,7 @@ declare namespace Store {
 | 
			
		||||
  type ActionChan = { Name: "Chan"; Data: number };
 | 
			
		||||
  type ActionFreq = { Name: "Freq"; Data: number };
 | 
			
		||||
  type ActionStim = { Name: "Stim"; Data: number };
 | 
			
		||||
  type ActionShowCursor = {Name: "ShowCursor", Data:boolean};
 | 
			
		||||
  type ActionShowAnswer = {Name: "ShowAnswer", Data:boolean};
 | 
			
		||||
  type Action = ActionMark | ActionTest | ActionChan | ActionFreq | ActionStim | ActionShowCursor | ActionShowAnswer;
 | 
			
		||||
  type Action = ActionMark | ActionTest | ActionChan | ActionFreq | ActionStim;
 | 
			
		||||
  type Reducer = (inState: State, inAction: Action) => State;
 | 
			
		||||
  type ContextUpdater = (inState: State) => boolean;
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { assertEquals } from "https://deno.land/std@0.166.0/testing/asserts.ts";
 | 
			
		||||
import { Reducer, ColumnMapping, Initial } from "../js/store.js";
 | 
			
		||||
import { Reducer, ColumnMapping, Initial } from "./src/store.js";
 | 
			
		||||
 | 
			
		||||
let state:Store.State = {
 | 
			
		||||
    Chan: { Min:0,   Max:1,   Value:0,  Step:1 },
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user