Skip to content

Commit

Permalink
feat(examples): add svg-waveform example
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed Apr 13, 2018
1 parent 584223c commit 1b21710
Show file tree
Hide file tree
Showing 14 changed files with 477 additions and 0 deletions.
29 changes: 29 additions & 0 deletions examples/svg-waveform/README.md
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
27 changes: 27 additions & 0 deletions examples/svg-waveform/package.json
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"
}
}
64 changes: 64 additions & 0 deletions examples/svg-waveform/src/api.ts
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;
}
88 changes: 88 additions & 0 deletions examples/svg-waveform/src/app.ts
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
}
}
24 changes: 24 additions & 0 deletions examples/svg-waveform/src/components/event-link.ts
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];
}
27 changes: 27 additions & 0 deletions examples/svg-waveform/src/components/main.ts
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,
}]
];
}
8 changes: 8 additions & 0 deletions examples/svg-waveform/src/components/sidebar.ts
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];
}
35 changes: 35 additions & 0 deletions examples/svg-waveform/src/components/slider.ts
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(),
}]]];
}
51 changes: 51 additions & 0 deletions examples/svg-waveform/src/components/waveform.ts
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)
);
}
Loading

0 comments on commit 1b21710

Please sign in to comment.