diff --git a/index.html b/index.html
new file mode 100644
index 00000000..1baabcb6
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Bingo
+
+
+
+
+
+
+
+
diff --git a/js/package.json b/js/package.json
new file mode 100644
index 00000000..e69de29b
diff --git a/js/strudel.js b/js/strudel.js
new file mode 100644
index 00000000..b01bea88
--- /dev/null
+++ b/js/strudel.js
@@ -0,0 +1,138 @@
+import Fraction from 'fraction.js'
+
+Fraction.prototype.sam = function() {
+ return Fraction(Math.floor(this))
+}
+
+Fraction.prototype.lt = function(other) {
+ return this.compare(other) < 0
+}
+
+Fraction.prototype.lte = function(other) {
+ return this.compare(other) <= 0
+}
+
+Fraction.prototype.gte = function(other) {
+ return this.compare(other) >= 0
+}
+
+class TimeSpan {
+ constructor(begin, end) {
+ this.begin = Fraction(begin)
+ this.end = Fraction(end)
+ }
+
+ get spanCycles() {
+ var spans = []
+ var begin = this.begin
+ var end = this.end
+ var end_sam = end.sam()
+
+ while (end.gt(begin)) {
+ // If begin and end are in the same cycle, we're done.
+ if (begin.sam() == end_sam) {
+ spans.push(new TimeSpan(begin, this.end))
+ break
+ }
+ // add a timespan up to the next sam
+ var next_begin = begin.next_sam()
+ spans.push(TimeSpan(begin, next_begin))
+
+ // continue with the next cycle
+ begin = next_begin
+ }
+ return(spans)
+ }
+
+ withTime(func_time) {
+ // Applies given function to both the begin and end time value of the timespan"""
+ return(new TimeSpan(func_time(this.begin), func_time(this.end)))
+ }
+
+ intersection(other) {
+ // Intersection of two timespans, returns None if they don't intersect.
+ intersect_begin = Math.max(this.begin, other.begin)
+ intersect_end = Math.min(this.end, other.end)
+
+ if (intersect_begin.gt(intersect_end)) {
+ return(undefined)
+ }
+ if (intersect_begin.equals(intersect_end)) {
+ // Zero-width (point) intersection - doesn't intersect if it's at the end of a
+ // non-zero-width timespan.
+ if (intersect_begin.equals(this.end) && this.begin.lt(this.end)) {
+ return(undefined)
+ }
+ if (intersect_begin.equals(other.end) && other.begin.lt(other.end)) {
+ return(undefined)
+ }
+ }
+ return(new TimeSpan(intersect_begin, intersect_end))
+ }
+
+ intersection_e(other) {
+ // Like 'sect', but raises an exception if the timespans don't intersect.
+ result = this.intersection(other)
+ if (result == None) {
+ // TODO - raise exception
+ // raise ValueError(f'TimeSpan {self} and TimeSpan {other} do not intersect')
+ }
+ return(result)
+ }
+
+ get midpoint() {
+ return(this.begin.add((this.end.sub(this.begin)).div(Fraction(2))))
+ }
+
+ equals(other) {
+ return this.begin.equals(other.begin) && this.end.equals(other.end)
+ }
+}
+
+class Event {
+
+ /*
+ Event class, representing a value active during the timespan
+ 'part'. This might be a fragment of an event, in which case the
+ timespan will be smaller than the 'whole' timespan, otherwise the
+ two timespans will be the same. The 'part' must never extend outside of the
+ 'whole'. If the event represents a continuously changing value
+ then the whole will be returned as None, in which case the given
+ value will have been sampled from the point halfway between the
+ start and end of the 'part' timespan.
+ */
+
+ constructor(whole, part, value) {
+ this.whole = whole
+ this.part = part
+ this.value = value
+ }
+
+ with_span(func) {
+ // Returns a new event with the function f applies to the event timespan.
+ var whole = this.whole ? func(self.whole) : undefined
+ return new Event(whole, func(this.part), this.value)
+ }
+
+ with_value(func) {
+ // Returns a new event with the function f applies to the event value.
+ return new Event(this.whole, this.part, func(this.value))
+ }
+
+ has_onset() {
+ // Test whether the event contains the onset, i.e that
+ // the beginning of the part is the same as that of the whole timespan."""
+ return (this.whole != undefined) && (this.whole.begin == this.part.begin)
+ }
+}
+
+window.bar = function() {
+ console.log("aha" + typeof(Fraction(3.5)));
+}
+
+//console.log((new TimeSpan(3.5,4)).midpoint);
+
+var ts = new TimeSpan(0,4)
+console.log((new Event(new TimeSpan(0,2), new TimeSpan(0,2), "aha")).has_onset());
+var x = new TimeSpan(0,4)
+console.log(ts.equals(x))
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..abe13d7b
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,53 @@
+{
+ "name": "strudel",
+ "version": "1.0.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "strudel",
+ "version": "1.0.0",
+ "license": "GPL-3.0-or-later",
+ "devDependencies": {
+ "fraction": "^0.2.0",
+ "fraction.js": "^4.1.2"
+ }
+ },
+ "node_modules/fraction": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/fraction/-/fraction-0.2.0.tgz",
+ "integrity": "sha1-ZassG7bt+JmJ2B1Q8FJeX6J7Hkw=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.2.tgz",
+ "integrity": "sha512-o2RiJQ6DZaR/5+Si0qJUIy637QMRudSi9kU/FFzx9EZazrIdnBgpU+3sEWCxAVhH2RtxW2Oz+T4p2o8uOPVcgA==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://www.patreon.com/infusion"
+ }
+ }
+ },
+ "dependencies": {
+ "fraction": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/fraction/-/fraction-0.2.0.tgz",
+ "integrity": "sha1-ZassG7bt+JmJ2B1Q8FJeX6J7Hkw=",
+ "dev": true
+ },
+ "fraction.js": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.2.tgz",
+ "integrity": "sha512-o2RiJQ6DZaR/5+Si0qJUIy637QMRudSi9kU/FFzx9EZazrIdnBgpU+3sEWCxAVhH2RtxW2Oz+T4p2o8uOPVcgA==",
+ "dev": true
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..9d246a81
--- /dev/null
+++ b/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "strudel",
+ "version": "1.0.0",
+ "description": "Experimental port of tidalcycles to javascript",
+ "main": "index.js",
+ "scripts": {
+ "test": "mocha"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/yaxu/strudel.git"
+ },
+ "keywords": [
+ "tidalcycles",
+ "strudel",
+ "pattern",
+ "livecoding",
+ "algorave"
+ ],
+ "author": "Alex McLean",
+ "license": "GPL-3.0-or-later",
+ "bugs": {
+ "url": "https://github.com/yaxu/strudel/issues"
+ },
+ "homepage": "https://github.com/yaxu/strudel#readme",
+ "devDependencies": {
+ "fraction.js": "^4.1.2"
+ }
+}