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};