Skip to content

Instantly share code, notes, and snippets.

@kefavn
Last active April 13, 2019 00:49
Show Gist options
  • Save kefavn/510381aeec7efd13a968569b9fd3f814 to your computer and use it in GitHub Desktop.
Save kefavn/510381aeec7efd13a968569b9fd3f814 to your computer and use it in GitHub Desktop.
Web Audio
import Component from '@ember/component';
import { action, computed } from '@ember-decorators/object';
import { className, classNames } from '@ember-decorators/component';
import { inject as service } from '@ember-decorators/service';
@classNames('x-audio-out')
export default class extends Component {
@service audio
@className
@computed('audio.isPlaying')
get active() {
return this.audio.isPlaying;
}
didInsertElement() {
this.elementUpdated(this.element);
}
click() {
if (this.audio.isPlaying) {
return this.audio.pause();
}
return this.audio.play();
}
};
import Component from '@ember/component';
import EmberObject from '@ember/object';
import { connectElements } from 'app/utils/svg-connect';
import { computed, action } from '@ember-decorators/object';
import { className, classNames } from '@ember-decorators/component';
import { inject as service } from '@ember-decorators/service';
@classNames('x-connector')
export default class extends Component {
constructor() {
super(...arguments);
this.stateMap = EmberObject.create({});
this.stateElementMap = EmberObject.create({});
}
didRender() {
this.updateSVG();
}
updateSVG() {
const controls = this.output;
if (!this.output) {
return;
}
const container = $(this.element.querySelector('.svgContainer'));
const s = $(this.element.querySelector('.svg1'));
const $controls = $(this.output);
Object.keys(this.stateMap).forEach(label => {
const element = $(`#${this.elementId}${label}`);
if (element.length) {
const el = this.stateElementMap[label];
connectElements(container, s, element, $(el), $controls);
}
});
}
@action
addInput(element, label) {
this.set(`stateMap.${label}`, true);
this.set(`stateElementMap.${label}`, element);
}
@action
addOutput(element) {
this.set('output', element);
}
@action
updateState(label, state) {
this.set(`stateMap.${label}`, state);
}
};
import Ember from 'ember';
import Component from '@ember/component';
import { computed } from '@ember-decorators/object';
import { htmlSafe } from '@ember/template';
import { restartableTask } from 'ember-concurrency-decorators';
import { debounce } from '@ember/runloop';
export default class extends Component {
constructor() {
super(...arguments);
this._prevValue = 0;
this.dragResistance = 3;
this.wheelResistance = 100;
this.integerOnly = true;
this._handlers = {
inputChange: this.handleInputChange.bind(this),
touchStart: this.handleTouchStart.bind(this),
touchMove: this.handleTouchMove.bind(this),
touchEnd: this.handleTouchEnd.bind(this),
touchCancel: this.handleTouchCancel.bind(this),
mouseDown: this.handleMouseDown.bind(this),
mouseMove: this.handleMouseMove.bind(this),
mouseUp: this.handleMouseUp.bind(this),
mouseWheel: this.handleMouseWheel.bind(this),
focus: this.handleFocus.bind(this),
blur: this.handleBlur.bind(this),
};
}
didInsertElement() {
super.didInsertElement(...arguments);
this.element.addEventListener('wheel', this._handlers.mouseWheel);
}
@computed('knobOffset')
get gripStyle() {
const deg = -132 + this.knobOffset * 264;
return htmlSafe(`transform: translate(-50%, -50%) rotate(${deg}deg);`);
}
@computed('value', 'min', 'max')
get knobOffset() {
const min = this.min;
const max = this.max;
// console.log(`V: ${this.value} Mi: ${min} ma: ${max}`);
return Math.abs(this.value - this.min) / (this.max - this.min);
}
@computed('knobOffset')
get gripStrokeOffset() {
const offset = (1 - this.knobOffset) * 184;
return htmlSafe(`stroke-dashoffset: ${offset}`);
}
mouseDown(event) {
return this.handleMouseDown(event);
}
touchStart(event) {
return this.handleTouchStart(event);
}
updateValue(value) {
this.onChange(value);
}
handleMouseDown(evt) {
// console.log('mouse down');
this.clearDrag();
evt.preventDefault();
this._activeDrag = true;
this.startDrag(evt.clientY);
// drag update/end listeners
document.body.addEventListener('mousemove', this._handlers.mouseMove);
document.body.addEventListener('mouseup', this._handlers.mouseUp);
}
// handlers
handleInputChange(evt) {
// console.log('input change');
this.updateToInputValue();
}
handleTouchStart(evt) {
// console.log('touch start');
this.clearDrag();
evt.preventDefault();
var touch = evt.changedTouches.item(evt.changedTouches.length - 1);
this._activeDrag = touch.identifier;
this.startDrag(touch.clientY);
// drag update/end listeners
document.body.addEventListener('touchmove', this._handlers.touchMove);
document.body.addEventListener('touchend', this._handlers.touchEnd);
document.body.addEventListener('touchcancel', this._handlers.touchCancel);
}
handleTouchMove(evt) {
// console.log('touch move');
var activeTouch = this.findActiveTouch(evt.changedTouches);
if (activeTouch) {
this.updateDrag(activeTouch.clientY);
} else if (!this.findActiveTouch(evt.touches)) {
this.clearDrag();
}
}
handleTouchEnd(evt) {
// console.log('touch end');
var activeTouch = this.findActiveTouch(evt.changedTouches);
if (activeTouch) {
this.finalizeDrag(activeTouch.clientY);
}
}
handleTouchCancel(evt) {
// console.log('touch cancel');
if (this.findActiveTouch(evt.changedTouches)) {
this.clearDrag();
}
}
handleMouseDown(evt) {
// console.log('mouse down');
this.clearDrag();
evt.preventDefault();
this._activeDrag = true;
this.startDrag(evt.clientY);
// drag update/end listeners
document.body.addEventListener('mousemove', this._handlers.mouseMove);
document.body.addEventListener('mouseup', this._handlers.mouseUp);
}
handleMouseMove(evt) {
// console.log('mouse move');
if (evt.buttons&1) {
this.updateDrag(evt.clientY);
} else {
this.finalizeDrag(evt.clientY);
}
}
handleMouseUp(evt) {
// console.log('mouse up');
this.finalizeDrag(evt.clientY);
}
handleMouseWheel(evt) {
// console.log('mouse wheel');
this.element.focus();
this.clearDrag();
this._prevValue = parseFloat(this.value);
this.updateFromDrag(evt.deltaY, this.wheelResistance);
}
handleDoubleClick(evt) {
// console.log('double click');
this.clearDrag();
this.updateValue(this.initial);
this.updateToInputValue();
}
handleFocus(evt) {
// console.log('focus on');
//this._container.classList.add('focus-active');
}
handleBlur(evt) {
// console.log('focus off');
//this._container.classList.remove('focus-active');
}
// dragging
startDrag(yPosition) {
this._dragStartPosition = yPosition;
this._prevValue = parseFloat(this.value);
this.element.focus();
}
updateDrag(yPosition) {
var diff = yPosition - this._dragStartPosition;
this.updateFromDrag(diff, this.dragResistance);
}
finalizeDrag(yPosition) {
var diff = yPosition - this._dragStartPosition;
this.updateFromDrag(diff, this.dragResistance);
this.clearDrag();
}
clearDrag() {
this._activeDrag = false;
// clean up event listeners
document.body.removeEventListener('mousemove', this._handlers.mouseMove);
document.body.removeEventListener('mouseup', this._handlers.mouseUp);
document.body.removeEventListener('touchmove', this._handlers.touchMove);
document.body.removeEventListener('touchend', this._handlers.touchEnd);
document.body.removeEventListener('touchcancel', this._handlers.touchCancel);
}
updateToInputValue() {
var normVal = this.normalizeValue(parseFloat(this._input.value));
this.updateVisuals(normVal);
}
updateFromDragTask(dragAmount, resistance) {
let value = this._prevValue - (dragAmount/resistance);
console.log(`DART: ${dragAmount} Res: ${resistance} V: ${value}`);
if (this.integerOnly) {
value = parseInt(value);
}
var clampedValue = this.clampValue(value);
this.updateValue(clampedValue);
}
updateFromDrag(dragAmount, resistance) {
//debounce(this, this.updateFromDragTask, dragAmount, resistance, 16);
this.updateFromDragTask(dragAmount, resistance);
}
// utils
clampValue(val) {
var min = parseFloat(this.min);
var max = parseFloat(this.max);
return Math.min(Math.max(val, min), max);
}
normalizeValue(val) {
var min = parseFloat(this.min);
var max = parseFloat(this.max);
return (val-min)/(max-min);
}
findActiveTouch(touchList) {
var i, len, touch;
for (i=0, len=touchList.length; i<len; i++)
if (this._activeDrag === touchList.item(i).identifier)
return touchList.item(i);
return null;
}
};
import Ember from 'ember';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
import { Oscillator } from 'app/utils/audio';
export default Ember.Component.extend({
classNames: ['panel'],
active: true,
audio: service(),
init() {
this._super(...arguments);
const audioContext = this.audio.audioContext;
const oscillator = new Oscillator({ out: audioContext.destination, audioContext });
this.audio.addOscillator(oscillator);
this.oscillator = oscillator;
if (this.startingNoteOffset) {
this.oscillator.changeNoteOffset(this.startingNoteOffset);
}
if (this.startingGain) {
this.oscillator.changeGain(this.startingGain);
}
},
didInsertElement() {
if (this.elementUpdated) {
this.elementUpdated(this.element.querySelector('.connectable'), this.label);
}
},
actions: {
changeNoteOffset(offset) {
this.oscillator.changeNoteOffset(offset);
},
changeLowOscillatorType(type) {
this.oscillator.changeWaveType(type);
},
changeGain(gain) {
this.oscillator.changeGain(gain);
},
setActive() {
this.toggleProperty('active');
this.updateState(this.label, this.active);
}
}
});
import Component from '@ember/component';
import EmberObject from '@ember/object';
import { connectElements } from 'app/utils/svg-connect';
import { computed, action } from '@ember-decorators/object';
import { className, classNames } from '@ember-decorators/component';
import { inject as service } from '@ember-decorators/service';
@classNames('x-wave-visualizer')
export default class extends Component {
@service audio
@computed()
get vElement() {
return this.element.querySelectorFor('.visualizer');
}
@computed('audio.analyser')
get visualizer() {
const analyser = this.audio.analyser;
}
};
import Mixin from '@ember/object/mixin';
import { inject as service } from '@ember-decorators/service';
export default class AudioNode extends Mixin {
@service audio;
node = null;
input = null;
output = null;
group = null;
constructor() {
super(...arguments);
if (!this.node || !this.group) {
return console.error('Missing node or group');
}
if (!this.input && !this.output) {
return console.error('Input/Output missing');
}
this.group.nodes.addObject(this.node);
}
willDestroyElement() {
this.group.nodes.removeObject(this.node);
}
};
import Ember from 'ember';
import { computed } from '@ember/object';
import Evented from '@ember/object/evented';
import { Note } from 'app/utils/frequency';
export default Ember.Service.extend(Evented, {
isPlaying: false,
oscillators: [],
audioContext: computed(function() {
return new (window.AudioContext || window.webkitAudioContext)();
}),
addOscillator(oscillator) {
this.oscillators.pushObject(oscillator);
},
play() {
this.set('isPlaying', true);
console.log(this.oscillators);
this.oscillators.forEach(oscillator => oscillator.on());
},
pause() {
this.oscillators.forEach(oscillator => oscillator.off());
this.set('isPlaying', false);
}
});
html {
--primary-color: #E4E8EA;
--background-color: #181B1C;
--color-dead: rgba(0,0,0,0.2);
--dark: rgba(0,0,0,0.3);
--darker: rgba(0,0,0,0.5);
}
body {
margin: 2em 2em;
font-family: monospace, Helvetica, Arial, sans-serif;
background-color: var(--background-color);
color: var(--primary-color);
}
button {
height: 24px;
}
.container {
margin: 2em 2em;
border-radius: 10px;
border-color: var(--primary-color);
border-width: 3px;
border-style: solid;
}
.container > h2 {
font-weight: 400;
padding-left: 30px;
padding-right: 30px;
margin-top: 0px;
transform: translate(0, -60%);
margin-bottom: 10px;
background-color: var(--background-color);
}
.section {
padding-left: 20px;
padding-right: 20px;
width: 100%;
}
.grid {
display: grid;
grid-template-rows: repeat(1, 480px);
grid-template-columns: repeat(3, 140px);
grid-column-gap: 20px;
grid-row-gap: 20px;
}
.grid-item {
z-index: 2;
grid-column-start: 1;
grid-column-end: 10;
grid-row-start: 1;
grid-row-end: 10;
}
.item-container {
width: 300px;
height: 300px;
border-radius: 5px;
border: 3px dashed #999;
}
.control-button {
font-weight: 400;
margin-left: 4px;
margin-right: 4px;
width: 24px;
height: 24px;
background-color: var(--dark);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
border: 1px rgba(0,0,0,0) solid;
cursor: pointer;
user-select: none;
border-radius: 3px;
}
.control-button.smallest {
font-size: 8px;
}
.control-button.small {
font-size: 14px;
}
.control-button.active {
background-color: var(--highlight-color, rgba(255,255,255,0.5));
}
.control-button:hover {
border: 1px var(--primary-color) solid;
}
.color-dead {
--highlight-color: var(--color-dead);
}
.white {
--highlight-color: white;
}
.orange {
--highlight-color: hsl(32, 95%, 50%);
}
.green {
--highlight-color: hsl(144, 95%, 38%);
}
.pink {
--highlight-color: hsl(313, 95%, 50%);
}
.blue {
--highlight-color: hsl(240, 95%, 50%);
}
.page {
display: block;
position: relative;
}
.center {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.center.row {
flex-direction: row;
}
.inline.center {
display: inline-flex;
}
.x-audio-out {
font-size: 48px;
border-radius: 50%;
text-align: center;
line-height: 80px;
width: 80px;
height: 80px;
opacity: 0.5;
user-select: none;
cursor: pointer;
background-color: var(--darker);
transition: 0.3s cubic-bezier(0, 0, 0.24, 1);
border: 2px solid var(--primary-color);
}
.x-audio-out.active {
opacity: 1;
background-color: var(--highlight-color, var(--primary-color));
transition: 0s;
animation: pulse-outer 2s linear infinite;
}
@keyframes pulse-outer {
0% {
box-shadow: 0 0 0 2px rgba(228,232,234, 0)
}
40% {
box-shadow: 0 0 0 4px rgba(228,232,234, 0.2)
}
70% {
box-shadow: 0 0 0 10px rgba(228,232,234, 0.12)
}
100% {
box-shadow: 0 0 0 12px rgba(228,232,234, 0)
}
}
.svgContainer {
z-index: -10;
position:absolute;
top: 0;
left: 0;
}
.svgContainer path {
stroke: white;
stroke-dashoffset: 100%;
stroke-dasharray: 100% 100%;
opacity: 0.25;
transition: 0.4s cubic-bezier(0, 0, 0.24, 1);
}
.svgContainer path.on {
stroke-dashoffset: 0%;
stroke: var(--primary-color);
opacity: 0.5;
}
.knob-container {
margin: 10px;
color: var(--highlight-color, white);
position: relative;
width: 100px;
height: 100px;
}
.knob-handle svg {
pointer-events: none;
position: absolute;
stroke-width: 8;
stroke-dasharray: 184 184;
stroke-width: 5;
stroke-color: currentColor;
}
.knob-handle svg path {
transition: 0.3s cubic-bezier(0, 0, 0.24, 1);
}
.knob-handle svg path.colored {
stroke: var(--highlight-color, white);
}
.knob-handle svg path.track {
stroke: rgba(255,255,255, 0.1);
}
.knob-grip {
position: absolute;
top: 50%;
left: 50%;
z-index: 5;
height: 82px;
width: 82px;
transition: 0.3s cubic-bezier(0, 0, 0.24, 1);
}
.knob-grip::after {
height: 20px;
width: 2px;
border-radius: 4px;
background-color: var(--highlight-color, white);
content: "";
position: absolute;
top: -1px;
left: 50%;
right: ;
bottom: ;
transform: translateX(-50%);
}
.knob-value {
position: absolute;
top: 50%;
left: 50%;
margin-top: -3px;
transform: translate(-50% , -50%);
}
.inner-panel .panel-title {
padding-bottom: 10px;
margin-bottom: 0px;
user-select: none;
cursor: pointer;
}
.inner-panel {
position: relative;
border-radius: 3px;
background-color: #2c2d2f;
}
.inner-panel > * {
width: 100%;
margin: 10px;
margin-top: 0px;
}
.inner-panel .panel-body,
.inner-panel .panel-title {
margin-top: 10px;
}
.inner-panel h4 {
border-bottom: 4px solid #181b1c;
}
.inner-panel .panel-row {
margin-bottom: 10px;
margin-top: 10px;
}
.panel-state {
position: absolute;
top: 5px;
left: 5px;
border-radius: 50%;
width: 8px;
height: 8px;
background-color: rgba(0,0,0,0.3);
}
.panel-state.active {
background-color: var(--highlight-color, var(--primary-color));
}
<div class="page">
<X-connector as |connector|>
<div class="container center inline">
<h2>
Web Audio
</h2>
<div class="section center">
<div class="grid">
<X-panel
@label="Low"
@elementUpdated={{action connector.actions.addInput}}
@updateState={{action connector.actions.updateState}}
@startingNoteOffset=-24
@startingGain=100
class="c--osc-1 pink"/>
<X-panel
@label="Medium"
@elementUpdated={{action connector.actions.addInput}}
@updateState={{action connector.actions.updateState}}
@startingGain=50
class="c--osc-2 green"/>
<X-panel
@label="High"
@elementUpdated={{action connector.actions.addInput}}
@updateState={{action connector.actions.updateState}}
@startingNoteOffset=24
@startingGain=25
class="c--osc-3 orange"/>
</div>
</div>
</div>
<X-audio-out @elementUpdated={{action connector.actions.addOutput}}/>
</X-connector/>
</div>
<div class="center">
{{yield (hash
actions=(hash
addInput=(action 'addInput')
addOutput=(action 'addOutput')
updateState=(action 'updateState')
)
)}}
</div>
<div class="svgContainer" style="margin: 50px 50px;">
<svg class="svg1" width="0" height="0" >
{{#each-in stateElementMap as |label|}}
<path
id={{concat this.elementId label}}
d="M0 0"
fill="none"
stroke-width="4px"
class="{{if (get stateMap label) 'on'}}"
/>
{{/each-in}}
<path
id="path2"
d="M0 0"
fill="none"
stroke-width="4px"
class="{{if (get stateMap "Medium") 'on'}}"
/>
<path
id="path3"
d="M0 0"
fill="none"
stroke-width="4px"
class="{{if (get stateMap "High") 'on'}}"
/>
</svg>
</div>
<div class="knob-container">
<div class="knob-track"></div>
<div class="knob-base">
</div>
<div class="knob-handle">
<svg width=100 height=100 viewBox="0 0 100 100" class="dial-svg">
<path class="track" d="M20,76 A 40 40 0 1 1 80 76" fill="none"/>
<path class="colored" d="M20,76 A 40 40 0 1 1 80 76" fill="none" style={{gripStrokeOffset}}/>
</svg>
</div>
<div class="knob-grip" style={{gripStyle}}></div>
<div class="knob-value">
{{@displayValue}}
</div>
</div>
<div class="center inner-panel {{if (not active) 'color-dead'}} connectable">
<h4 class="center panel-title" onclick={{action 'setActive'}}>
<div class="panel-state {{if active 'active'}}">
</div>
{{@label}}
</h4>
<div class="panel-body">
<div class="center">
<span>
Type
</span>
<div class="panel-row">
<div class="center row">
<div class="control-button {{if (eq oscillator.waveType 'sine') 'active'}}" {{action 'changeLowOscillatorType' 'sine'}}>
</div>
<div class="control-button small {{if (eq oscillator.waveType 'square') 'active'}}" {{action 'changeLowOscillatorType' 'square'}}>
</div>
<div class="control-button smallest {{if (eq oscillator.waveType 'sawtooth') 'active'}}" {{action 'changeLowOscillatorType' 'sawtooth'}}>
Saw
</div>
<div class="control-button small {{if (eq oscillator.waveType 'triangle') 'active'}}" {{action 'changeLowOscillatorType' 'triangle'}}>
</div>
</div>
</div>
</div>
<div class="center">
<span>
Frequency
</span>
<X-knob
@min=-48
@max=48
@value={{oscillator.noteOffset}}
@displayValue={{oscillator.noteOffset}}
@onChange={{action 'changeNoteOffset'}}
/>
</div>
<div class="center {{if active 'white' 'color-dead'}}">
<span>
Gain
</span>
<X-knob
@min=0
@max=100
@value={{oscillator.gain}}
@displayValue={{oscillator.gain}}
@onChange={{action 'changeGain'}}
/>
</div>
</div>
</div>
{{yield}}
<canvas class="visualizer" width=100 height=30/>
{
"version": "0.15.1",
"EmberENV": {
"FEATURES": {}
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js",
"ember": "3.4.3",
"ember-template-compiler": "3.4.3",
"ember-testing": "3.4.3"
},
"addons": {
"ember-data": "3.4.2",
"ember-decorators": "2.0.0",
"ember-truth-helpers": "2.1.0",
"ember-contextual-states": "0.1.6",
"ember-animated": "0.5.1",
"ember-decorators": "5.2.0",
"ember-concurrency": "0.9.0",
"ember-concurrency-decorators": "0.6.0"
}
}
export default class {
nodes = [];
}
import { Note } from 'app/utils/frequency';
import { computed } from '@ember-decorators/object';
import EmberObject from '@ember/object';
export class Oscillator extends EmberObject {
startedBefore = false;
active = false;
oscillatorNode = null;
waveType = 'sine';
out = null;
audioContext = null;
noteOffset = 0;
@computed('noteOffset')
get note() {
return new Note(this.noteOffset);
}
@computed('note')
get frequency() {
return this.note.frequency;
}
constructor({ gain, waveType, out, audioContext }) {
super(...arguments);
this.gain = gain || 50;
this.waveType = waveType || 'sine';
this.out = out;
this.audioContext = audioContext;
this.setupOscillator();
}
setupOscillator() {
this.oscillatorNode = this.createOscillatorNode();
this.gainNode = this.createGainNode();
this.changeGain(this.gain);
this.oscillatorNode.connect(this.gainNode);
this.gainNode.connect(this.out);
}
createGainNode() {
return this.audioContext.createGain();
}
createOscillatorNode() {
const oscillator = this.audioContext.createOscillator();
oscillator.type = this.waveType;
oscillator.frequency.setValueAtTime(this.frequency, this.audioContext.currentTime);
return oscillator;
}
changeWaveType(type) {
this.set('waveType', type);
console.log(type);
if (this.active) {
this.off();
this.on();
} else {
this.setupOscillator();
}
}
changeNoteOffset(offset) {
this.set('noteOffset', offset);
this.oscillatorNode.frequency.exponentialRampToValueAtTime(this.frequency, this.audioContext.currentTime + 0.001)
}
changeGain(gain) {
this.set('gain', gain);
this.gainNode.gain.value = gain / 100;
}
on() {
this.active = true;
this.oscillatorNode.start();
}
off() {
this.oscillatorNode.stop();
this.gainNode.disconnect();
this.oscillatorNode.disconnect();
this.setupOscillator();
this.active = false;
}
}
const LETTERS = ['A', 'A½', 'B', 'C', 'C½', 'D', 'D½', 'E', 'F', 'F½', 'G', 'G½'];
export class Note {
constructor(offsetFromMiddleA) {
this.noteOffset = parseInt(offsetFromMiddleA);
}
get frequency() {
return 440 * Math.pow(2, this.noteOffset/12);
}
get frequencyDisplay() {
return this.frequency.toFixed(2);
}
get octave() {
return 4 + parseInt(this.noteOffset/12);
}
get letterOffset() {
return (120 +this.noteOffset + 120) % 12;
}
get letter() {
return LETTERS[this.letterOffset];
}
toString() {
return `${this.letter}${String.fromCharCode(this.octave + 8320)}`;
}
}
function signum(x) {
return (x < 0) ? -1 : 1;
}
function absolute(x) {
return (x < 0) ? -x : x;
}
function drawPath(svg, path, startX, startY, endX, endY) {
// get the path's stroke width (if one wanted to be really precize, one could use half the stroke size)
var stroke = parseFloat(path.attr("stroke-width"));
// check if the svg is big enough to draw the path, if not, set heigh/width
if (svg.attr("height") < endY) svg.attr("height", endY);
if (svg.attr("width" ) < (startX + stroke) ) svg.attr("width", (startX + stroke));
if (svg.attr("width" ) < (endX + stroke) ) svg.attr("width", (endX + stroke));
var deltaX = (endX - startX) * 0.15;
var deltaY = (endY - startY) * 0.15;
// for further calculations which ever is the shortest distance
var delta = deltaY < absolute(deltaX) ? deltaY : absolute(deltaX);
// set sweep-flag (counter/clock-wise)
// if start element is closer to the left edge,
// draw the first arc counter-clockwise, and the second one clock-wise
var arc1 = 0; var arc2 = 1;
if (startX > endX) {
arc1 = 1;
arc2 = 0;
}
// draw tha pipe-like path
// 1. move a bit down, 2. arch, 3. move a bit to the right, 4.arch, 5. move down to the end
path.attr("d", "M" + startX + " " + startY +
" V" + (startY + delta) +
" A" + delta + " " + delta + " 0 0 " + arc1 + " " + (startX + delta*signum(deltaX)) + " " + (startY + 2*delta) +
" H" + (endX - delta*signum(deltaX)) +
" A" + delta + " " + delta + " 0 0 " + arc2 + " " + endX + " " + (startY + 3*delta) +
" V" + endY );
}
export function connectElements(svgContainer, svg, path, startElem, endElem) {
//console.log(`c: ${svgContainer} s: ${svg} p: ${path} se ${startElem} e ${endElem}`);
// if first element is lower than the second, swap!
if(startElem.offset().top > endElem.offset().top){
var temp = startElem;
startElem = endElem;
endElem = temp;
}
// get (top, left) corner coordinates of the svg container
var svgTop = svgContainer.offset().top;
var svgLeft = svgContainer.offset().left;
// get (top, left) coordinates for the two elements
var startCoord = startElem.offset();
var endCoord = endElem.offset();
// calculate path's start (x,y) coords
// we want the x coordinate to visually result in the element's mid point
var startX = startCoord.left + 0.5*startElem.outerWidth() - svgLeft; // x = left offset + 0.5*width - svg's left offset
var startY = startCoord.top + startElem.outerHeight() - svgTop; // y = top offset + height - svg's top offset
// calculate path's end (x,y) coords
var endX = endCoord.left + 0.5*endElem.outerWidth() - svgLeft;
var endY = endCoord.top - svgTop;
// call function for drawing the path
drawPath(svg, path, startX, startY, endX, endY);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment