-
-
Notifications
You must be signed in to change notification settings - Fork 153
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(examples): add svg-waveform example
- Loading branch information
1 parent
584223c
commit 1b21710
Showing
14 changed files
with
477 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# svg-waveform | ||
## About | ||
|
||
TODO | ||
|
||
## Building | ||
### Development | ||
|
||
``` | ||
git clone https://github.com/[your-gh-username]/rs-icep | ||
yarn install | ||
yarn start | ||
``` | ||
|
||
Installs all dependencies, runs `webpack-dev-server` and opens the app in your browser. | ||
|
||
### Production | ||
|
||
``` | ||
yarn build | ||
``` | ||
|
||
Builds a minified version of the app and places it in `/public` directory. | ||
|
||
## Authors | ||
|
||
TODO | ||
|
||
© 2018 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{ | ||
"name": "svg-waveform", | ||
"version": "0.0.1", | ||
"description": "TODO", | ||
"repository": "https://github.com/[your-gh-username]/rs-icep", | ||
"author": "TODO", | ||
"license": "MIT", | ||
"scripts": { | ||
"build": "webpack --mode production", | ||
"start": "webpack-dev-server --open --mode development --devtool inline-source-map" | ||
}, | ||
"dependencies": { | ||
"@thi.ng/atom": "latest", | ||
"@thi.ng/hdom": "latest", | ||
"@thi.ng/hiccup-svg": "latest", | ||
"@thi.ng/interceptors": "latest", | ||
"@thi.ng/iterators": "latest" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^9.6.2", | ||
"typescript": "^2.8.1", | ||
"ts-loader": "^4.1.0", | ||
"webpack": "^4.5.0", | ||
"webpack-cli": "^2.0.14", | ||
"webpack-dev-server": "^3.1.1" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { IObjectOf } from "@thi.ng/api/api"; | ||
import { ViewTransform, IView } from "@thi.ng/atom/api"; | ||
import { EventDef, EffectDef } from "@thi.ng/interceptors/api"; | ||
import { EventBus } from "@thi.ng/interceptors/event-bus"; | ||
|
||
/** | ||
* Function signature for main app components. | ||
*/ | ||
export type AppComponent = (ctx: AppContext, ...args: any[]) => any; | ||
|
||
/** | ||
* Derived view configurations. | ||
*/ | ||
export type ViewSpec = string | [string, ViewTransform<any>]; | ||
|
||
/** | ||
* Structure of the overall application config object. | ||
* See `src/config.ts`. | ||
*/ | ||
export interface AppConfig { | ||
events: IObjectOf<EventDef>; | ||
effects: IObjectOf<EffectDef>; | ||
domRoot: string | Element; | ||
initialState: any; | ||
rootComponent: AppComponent; | ||
ui: UIAttribs; | ||
views: Partial<Record<keyof AppViews, ViewSpec>>; | ||
} | ||
|
||
/** | ||
* Base structure of derived views exposed by the base app. | ||
* Add more declarations here as needed. | ||
*/ | ||
export interface AppViews extends Record<keyof AppViews, IView<any>> { | ||
amp: IView<number>; | ||
freq: IView<number>; | ||
phase: IView<number>; | ||
harmonics: IView<number>; | ||
hstep: IView<number>; | ||
} | ||
|
||
/** | ||
* Helper interface to pre-declare keys of shared UI attributes for | ||
* components and so enable autocomplete & type safety. | ||
* | ||
* See `AppConfig` above and its use in `src/config.ts` and various | ||
* component functions. | ||
*/ | ||
export interface UIAttribs { | ||
link: any; | ||
slider: { root: any, range: any, number: any }; | ||
root: any; | ||
sidebar: any; | ||
wave: any; | ||
} | ||
|
||
/** | ||
* Structure of the context object passed to all component functions | ||
*/ | ||
export interface AppContext { | ||
bus: EventBus; | ||
views: AppViews; | ||
ui: UIAttribs; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import { IObjectOf } from "@thi.ng/api/api"; | ||
import { Atom } from "@thi.ng/atom/atom"; | ||
import { isArray } from "@thi.ng/checks/is-array"; | ||
import { start } from "@thi.ng/hdom"; | ||
import { EventBus } from "@thi.ng/interceptors/event-bus"; | ||
|
||
import { AppConfig, AppContext, AppViews, ViewSpec } from "./api"; | ||
|
||
/** | ||
* Generic base app skeleton. You can use this as basis for your own | ||
* apps. | ||
* | ||
* As is the app does not much more than: | ||
* | ||
* - initialize state and event bus | ||
* - attach derived views | ||
* - define root component wrapper | ||
* - start hdom render & event bus loop | ||
*/ | ||
export class App { | ||
|
||
config: AppConfig; | ||
ctx: AppContext; | ||
state: Atom<any>; | ||
|
||
constructor(config: AppConfig) { | ||
this.config = config; | ||
this.state = new Atom(config.initialState || {}); | ||
this.ctx = { | ||
bus: new EventBus(this.state, config.events, config.effects), | ||
views: <AppViews>{}, | ||
ui: config.ui, | ||
}; | ||
this.addViews(this.config.views); | ||
} | ||
|
||
/** | ||
* Initializes given derived view specs and attaches them to app | ||
* state atom. | ||
* | ||
* @param specs | ||
*/ | ||
addViews(specs: IObjectOf<ViewSpec>) { | ||
const views = this.ctx.views; | ||
for (let id in specs) { | ||
const spec = specs[id]; | ||
if (isArray(spec)) { | ||
views[id] = this.state.addView(spec[0], spec[1]); | ||
} else { | ||
views[id] = this.state.addView(spec); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Calls `init()` and kicks off hdom render loop, including batched | ||
* event processing and fast fail check if DOM updates are necessary | ||
* (assumes ALL state is held in the app state atom). So if there | ||
* weren't any events causing a state change since last frame, | ||
* re-rendering is skipped without even attempting to diff DOM | ||
* tree). | ||
*/ | ||
start() { | ||
this.init(); | ||
// assume main root component is a higher order function | ||
// call it here to pre-initialize it | ||
const root = this.config.rootComponent(this.ctx); | ||
let firstFrame = true; | ||
start( | ||
this.config.domRoot, | ||
() => { | ||
if (this.ctx.bus.processQueue() || firstFrame) { | ||
firstFrame = false; | ||
return root(); | ||
} | ||
}, | ||
this.ctx | ||
); | ||
} | ||
|
||
/** | ||
* User initialization hook. | ||
* Automatically called from `start()` | ||
*/ | ||
init() { | ||
// ...add init tasks here | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { Event } from "@thi.ng/interceptors/api"; | ||
|
||
import { AppContext } from "../api"; | ||
|
||
/** | ||
* Customizable hyperlink component emitting given event on event bus | ||
* when clicked. | ||
* | ||
* @param ctx | ||
* @param event event tuple of `[event-id, payload]` | ||
* @param attribs element attribs | ||
* @param body link body | ||
*/ | ||
export function eventLink(ctx: AppContext, attribs: any, event: Event, body: any) { | ||
return ["a", | ||
{ | ||
...attribs, | ||
onclick: (e) => { | ||
e.preventDefault(); | ||
ctx.bus.dispatch(event); | ||
} | ||
}, | ||
body]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { AppContext } from "../api"; | ||
import * as ev from "../events"; | ||
|
||
import { sidebar } from "./sidebar"; | ||
import { waveform } from "./waveform"; | ||
|
||
export function main(ctx: AppContext) { | ||
const bar = sidebar(ctx, | ||
{ event: ev.SET_PHASE, view: "phase", label: "phase", max: 360 }, | ||
{ event: ev.SET_FREQ, view: "freq", label: "frequency", max: 10, step: 0.01 }, | ||
{ event: ev.SET_AMP, view: "amp", label: "amplitude", max: 4, step: 0.01 }, | ||
{ event: ev.SET_HARMONICS, view: "harmonics", label: "harmonics", min: 1, max: 20 }, | ||
{ event: ev.SET_HSTEP, view: "hstep", label: "h step", min: 1, max: 3, step: 0.01 }, | ||
); | ||
return () => [ | ||
"div", ctx.ui.root, | ||
bar, | ||
[waveform, { | ||
phase: ctx.views.phase.deref(), | ||
freq: ctx.views.freq.deref(), | ||
amp: ctx.views.amp.deref(), | ||
harmonics: ctx.views.harmonics.deref(), | ||
hstep: ctx.views.hstep.deref(), | ||
res: 500, | ||
}] | ||
]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { AppContext } from "../api"; | ||
|
||
import { slider, SliderOpts } from "./slider"; | ||
|
||
export function sidebar(ctx: AppContext, ...specs: SliderOpts[]) { | ||
const sliders = specs.map((s) => slider(ctx, s)); | ||
return () => ["div", ctx.ui.sidebar, ...sliders]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { AppContext } from "../api"; | ||
|
||
export interface SliderOpts { | ||
event: PropertyKey; | ||
view: PropertyKey; | ||
label: string; | ||
min?: number; | ||
max?: number; | ||
step?: number; | ||
} | ||
|
||
export function slider(ctx: AppContext, opts: SliderOpts) { | ||
const listener = (e) => ctx.bus.dispatch([opts.event, parseFloat(e.target.value)]); | ||
opts = Object.assign({ | ||
oninput: listener, | ||
min: 0, | ||
max: 100, | ||
step: 1, | ||
}, opts); | ||
return () => ["section", ctx.ui.slider.root, | ||
["input", | ||
{ | ||
...ctx.ui.slider.range, | ||
...opts, | ||
type: "range", | ||
value: ctx.views[opts.view].deref(), | ||
}], | ||
["div", opts.label, | ||
["input", { | ||
...ctx.ui.slider.number, | ||
...opts, | ||
type: "number", | ||
value: ctx.views[opts.view].deref(), | ||
}]]]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { svgdoc } from "@thi.ng/hiccup-svg/doc"; | ||
import { polyline } from "@thi.ng/hiccup-svg/polyline"; | ||
import { map } from "@thi.ng/iterators/map"; | ||
import { range } from "@thi.ng/iterators/range"; | ||
import { reduce } from "@thi.ng/iterators/reduce"; | ||
|
||
import { AppContext } from "../api"; | ||
|
||
const TAU = Math.PI * 2; | ||
|
||
export interface WaveformOpts { | ||
phase: number; | ||
freq: number; | ||
amp: number; | ||
harmonics: number; | ||
hstep: number; | ||
res: number; | ||
osc: number; | ||
} | ||
|
||
export function waveform(ctx: AppContext, opts: WaveformOpts) { | ||
const phase = opts.phase * Math.PI / 180; | ||
const amp = opts.amp * 50; | ||
const fscale = 1 / opts.res * TAU * opts.freq; | ||
return svgdoc( | ||
{ class: "w-100 h-100", viewBox: `0 -5 ${opts.res} 10` }, | ||
polyline( | ||
[ | ||
[0, 0], | ||
...map( | ||
(x) => [x, osc(x, phase, fscale, amp, opts.harmonics, opts.hstep)], | ||
range(opts.res) | ||
), | ||
[opts.res, 0] | ||
], | ||
ctx.ui.wave, | ||
) | ||
); | ||
} | ||
|
||
function osc(x: number, phase: number, fscale: number, amp: number, harmonics: number, hstep: number) { | ||
const f = x * fscale; | ||
return reduce( | ||
(sum, i) => { | ||
const k = (1 + i * hstep); | ||
return sum + Math.sin(phase + f * k) * amp / k; | ||
}, | ||
0, | ||
range(0, harmonics + 1) | ||
); | ||
} |
Oops, something went wrong.