jevstrudel.git / packages / draw / spiral.mjs
1import { Pattern } from '@strudel/core';
2import { getTheme } from './draw.mjs';
3
4// polar coords -> xy
5function fromPolar(angle, radius, cx, cy) {
6  const radians = ((angle - 90) * Math.PI) / 180;
7  return [cx + Math.cos(radians) * radius, cy + Math.sin(radians) * radius];
8}
9
10const xyOnSpiral = (angle, margin, cx, cy, rotate = 0) => fromPolar((angle + rotate) * 360, margin * angle, cx, cy); // TODO: logSpiral
11
12// draw spiral / segment of spiral
13function spiralSegment(options) {
14  let {
15    ctx,
16    from = 0,
17    to = 3,
18    margin = 50,
19    cx = 100,
20    cy = 100,
21    rotate = 0,
22    thickness = margin / 2,
23    color = getTheme().foreground,
24    cap = 'round',
25    stretch = 1,
26    fromOpacity = 1,
27    toOpacity = 1,
28  } = options;
29  from *= stretch;
30  to *= stretch;
31  rotate *= stretch;
32  ctx.lineWidth = thickness;
33  ctx.lineCap = cap;
34  ctx.strokeStyle = color;
35  ctx.globalAlpha = fromOpacity;
36
37  ctx.beginPath();
38  let [sx, sy] = xyOnSpiral(from, margin, cx, cy, rotate);
39  ctx.moveTo(sx, sy);
40
41  const increment = 1 / 60;
42  let angle = from;
43  while (angle <= to) {
44    const [x, y] = xyOnSpiral(angle, margin, cx, cy, rotate);
45    //ctx.lineWidth = angle*thickness;
46    ctx.globalAlpha = ((angle - from) / (to - from)) * toOpacity;
47    ctx.lineTo(x, y);
48    angle += increment;
49  }
50  ctx.stroke();
51}
52
53function drawSpiral(options) {
54  let {
55    stretch = 1,
56    size = 80,
57    thickness = size / 2,
58    cap = 'butt', // round butt squar,
59    inset = 3, // start angl,
60    playheadColor = '#ffffff',
61    playheadLength = 0.02,
62    playheadThickness = thickness,
63    padding = 0,
64    steady = 1,
65    activeColor = getTheme().foreground,
66    inactiveColor = getTheme().gutterForeground,
67    colorizeInactive = 0,
68    fade = true,
69    // logSpiral = true,
70    ctx,
71    time,
72    haps,
73    drawTime,
74    id,
75  } = options;
76
77  if (id) {
78    haps = haps.filter((hap) => hap.hasTag(id));
79  }
80
81  const [w, h] = [ctx.canvas.width, ctx.canvas.height];
82  ctx.clearRect(0, 0, w * 2, h * 2);
83  const [cx, cy] = [w / 2, h / 2];
84  const settings = {
85    margin: size / stretch,
86    cx,
87    cy,
88    stretch,
89    cap,
90    thickness,
91  };
92
93  const playhead = {
94    ...settings,
95    thickness: playheadThickness,
96    from: inset - playheadLength,
97    to: inset,
98    color: playheadColor,
99  };
100
101  const [min] = drawTime;
102  const rotate = steady * time;
103  haps.forEach((hap) => {
104    const isActive = hap.whole.begin <= time && hap.endClipped > time;
105    const from = hap.whole.begin - time + inset;
106    const to = hap.endClipped - time + inset - padding;
107    const hapColor = hap.value?.color || activeColor;
108    const color = colorizeInactive || isActive ? hapColor : inactiveColor;
109    const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
110    spiralSegment({
111      ctx,
112      ...settings,
113      from,
114      to,
115      rotate,
116      color,
117      fromOpacity: opacity,
118      toOpacity: opacity,
119    });
120  });
121  spiralSegment({
122    ctx,
123    ...playhead,
124    rotate,
125  });
126}
127
128/**
129 * Displays a spiral visual.
130 *
131 * @name spiral
132 * @tags visualization
133 * @param {Object} options Object containing all the optional following parameters as key value pairs:
134 * @param {number} stretch controls the rotations per cycle ratio, where 1 = 1 cycle / 360 degrees
135 * @param {number} size the diameter of the spiral
136 * @param {number} thickness line thickness
137 * @param {string} cap style of line ends: butt (default), round, square
138 * @param {string} inset number of rotations before spiral starts (default 3)
139 * @param {string} playheadColor color of playhead, defaults to white
140 * @param {number} playheadLength length of playhead in rotations, defaults to 0.02
141 * @param {number} playheadThickness thickness of playheadrotations, defaults to thickness
142 * @param {number} padding space around spiral
143 * @param {number} steady steadyness of spiral vs playhead. 1 = spiral doesn't move, playhead does.
144 * @param {number} activeColor color of active segment. defaults to foreground of theme
145 * @param {number} inactiveColor color of inactive segments. defaults to gutterForeground of theme
146 * @param {boolean} colorizeInactive wether or not to colorize inactive segments, defaults to 0
147 * @param {boolean} fade wether or not past and future should fade out. defaults to 1
148 * @param {boolean} logSpiral wether or not the spiral should be logarithmic. defaults to 0
149 * @example
150 * note("c2 a2 eb2")
151 * .euclid(5,8)
152 * .s('sawtooth')
153 * .lpenv(4).lpf(300)
154 * ._spiral({ steady: .96 })
155 */
156Pattern.prototype.spiral = function (options = {}) {
157  return this.onPaint((ctx, time, haps, drawTime) => drawSpiral({ ctx, time, haps, drawTime, ...options }));
158};