From 5cbe30095ba35a9530b4e1ad1a451ace89c7e2bd Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 24 Aug 2023 12:21:29 +0200 Subject: [PATCH 001/161] control osc partial count with n --- packages/superdough/helpers.mjs | 11 ------ packages/superdough/synth.mjs | 60 +++++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 7cc54c8d..651fc85c 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -6,17 +6,6 @@ export function gainNode(value) { return node; } -export const getOscillator = ({ s, freq, t }) => { - // make oscillator - const o = getAudioContext().createOscillator(); - o.type = s || 'triangle'; - o.frequency.value = Number(freq); - o.start(t); - //o.stop(t + duration + release); - const stop = (time) => o.stop(time); - return { node: o, stop }; -}; - // alternative to getADSR returning the gain node and a stop handle to trigger the release anytime in the future export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => { const gainNode = getAudioContext().createGain(); diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 57317133..9ad347e3 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,6 +1,6 @@ import { midiToFreq, noteToMidi } from './util.mjs'; import { registerSound, getAudioContext } from './superdough.mjs'; -import { getOscillator, gainNode, getEnvelope } from './helpers.mjs'; +import { gainNode, getEnvelope } from './helpers.mjs'; const mod = (freq, range = 1, type = 'sine') => { const ctx = getAudioContext(); @@ -36,17 +36,17 @@ export function registerSynthSounds() { } = value; let { n, note, freq } = value; // with synths, n and note are the same thing - n = note || n || 36; - if (typeof n === 'string') { - n = noteToMidi(n); // e.g. c3 => 48 + note = note || 36; + if (typeof note === 'string') { + note = noteToMidi(note); // e.g. c3 => 48 } // get frequency - if (!freq && typeof n === 'number') { - freq = midiToFreq(n); // + 48); + if (!freq && typeof note === 'number') { + freq = midiToFreq(note); // + 48); } // maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default) // make oscillator - const { node: o, stop } = getOscillator({ t, s: wave, freq }); + const { node: o, stop } = getOscillator({ t, s: wave, freq, partials: n }); let stopFm; if (fmModulationIndex) { @@ -76,3 +76,49 @@ export function registerSynthSounds() { ); }); } + +export function waveformN(partials, type) { + const real = new Float32Array(partials + 1); + const imag = new Float32Array(partials + 1); + const ac = getAudioContext(); + const osc = ac.createOscillator(); + + const amplitudes = { + sawtooth: (n) => 1 / n, + square: (n) => (n % 2 === 0 ? 0 : 1 / n), + triangle: (n) => (n % 2 === 0 ? 0 : 1 / (n * n)), + }; + + if (!amplitudes[type]) { + throw new Error(`unknown wave type ${type}`); + } + + real[0] = 0; // dc offset + imag[0] = 0; + let n = 1; + while (n <= partials) { + real[n] = amplitudes[type](n); + imag[n] = 0; + n++; + } + + const wave = ac.createPeriodicWave(real, imag); + osc.setPeriodicWave(wave); + return osc; +} + +export function getOscillator({ s, freq, t, partials }) { + // make oscillator + let o; + if (!partials || s === 'sine') { + o = getAudioContext().createOscillator(); + o.type = s || 'triangle'; + } else { + o = waveformN(partials, s); + } + o.frequency.value = Number(freq); + o.start(t); + //o.stop(t + duration + release); + const stop = (time) => o.stop(time); + return { node: o, stop }; +} From 7370f41fa0556406109be259cd24387d4a8bff78 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 25 Aug 2023 09:45:30 +0200 Subject: [PATCH 002/161] basic scope feature --- packages/core/controls.mjs | 3 ++ packages/core/draw.mjs | 2 +- packages/core/index.mjs | 1 + packages/core/scope.mjs | 48 ++++++++++++++++++++++++++++++ packages/superdough/superdough.mjs | 39 +++++++++++++++++++++++- 5 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 packages/core/scope.mjs diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index b8edd51c..a02a5805 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -146,6 +146,9 @@ const generic_params = [ */ ['bank'], + ['analyze'], // sends + ['fft'], + /** * Amplitude envelope decay time: the time it takes after the attack time to reach the sustain level. * Note that the decay is only audible if the sustain value is lower than 1. diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index 5ea0c591..8be45e03 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -27,7 +27,7 @@ export const getDrawContext = (id = 'test-canvas') => { return canvas.getContext('2d'); }; -Pattern.prototype.draw = function (callback, { from, to, onQuery }) { +Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) { if (window.strudelAnimation) { cancelAnimationFrame(window.strudelAnimation); } diff --git a/packages/core/index.mjs b/packages/core/index.mjs index bed63f9a..ceffdcbe 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -22,6 +22,7 @@ export * from './cyclist.mjs'; export * from './logger.mjs'; export * from './time.mjs'; export * from './draw.mjs'; +export * from './scope.mjs'; export * from './animate.mjs'; export * from './pianoroll.mjs'; export * from './spiral.mjs'; diff --git a/packages/core/scope.mjs b/packages/core/scope.mjs new file mode 100644 index 00000000..62b08e62 --- /dev/null +++ b/packages/core/scope.mjs @@ -0,0 +1,48 @@ +import { Pattern } from './pattern.mjs'; +import { getDrawContext } from './draw.mjs'; +import { analyser } from '@strudel.cycles/webaudio'; + +export function drawTimeScope(analyser, dataArray, { align = true, color = 'white', thickness = 2 } = {}) { + const canvasCtx = getDrawContext(); + + canvasCtx.lineWidth = thickness; + canvasCtx.strokeStyle = color; + + canvasCtx.beginPath(); + let canvas = canvasCtx.canvas; + + const bufferSize = analyser.frequencyBinCount; + const triggerValue = 256 / 2; + const triggerIndex = align + ? Array.from(dataArray).findIndex((v, i, arr) => i && arr[i - 1] < triggerValue && v >= triggerValue) + : 0; + + const sliceWidth = (canvas.width * 1.0) / bufferSize; + let x = 0; + + for (let i = triggerIndex; i < bufferSize; i++) { + const v = dataArray[i] / 128.0; + const y = (v * (canvas.height / 2)) / 2 + canvas.height / 2; + if (i === 0) { + canvasCtx.moveTo(x, y); + } else { + canvasCtx.lineTo(x, y); + } + x += sliceWidth; + } + canvasCtx.stroke(); +} + +Pattern.prototype.scope = function (config = {}) { + return this.analyze(1).draw((ctx) => { + let data = getAnalyzerData('time'); + const { smear = 0 } = config; + if (!smear) { + ctx.clearRect(0, 0, window.innerWidth, window.innerHeight); + } else { + ctx.fillStyle = `rgba(0,0,0,${1 - smear})`; + ctx.fillRect(0, 0, window.innerWidth, window.innerHeight); + } + data && drawTimeScope(analyser, data, config); + }); +}; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 1279000c..07b32eec 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -121,6 +121,34 @@ function getReverb(orbit, duration = 2) { return reverbs[orbit]; } +export let analyser, analyserData /* s = {} */; +export function getAnalyser(/* orbit, */ fftSize = 2048) { + if (!analyser /*s [orbit] */) { + const analyserNode = getAudioContext().createAnalyser(); + analyserNode.fftSize = fftSize; + // getDestination().connect(analyserNode); + analyser /* s[orbit] */ = analyserNode; + analyserData = new Uint8Array(analyser.frequencyBinCount); + } + if (analyser /* s[orbit] */.fftSize !== fftSize) { + analyser /* s[orbit] */.fftSize = fftSize; + analyserData = new Uint8Array(analyser.frequencyBinCount); + } + return analyser /* s[orbit] */; +} + +export function getAnalyzerData(type = 'time') { + const getter = { + time: () => analyser?.getByteTimeDomainData(analyserData), + frequency: () => analyser?.getByteFrequencyData(analyserData), + }[type]; + if (!getter) { + throw new Error(`getAnalyzerData: ${type} not supported. use one of ${Object.keys(getter).join(', ')}`); + } + getter(); + return analyserData; +} + function effectSend(input, effect, wet) { const send = gainNode(wet); input.connect(send); @@ -167,6 +195,8 @@ export const superdough = async (value, deadline, hapDuration) => { room, size = 2, velocity = 1, + analyze, // analyser wet + fft = 8, // fftSize 0 - 10 } = value; gain *= velocity; // legacy fix for velocity let toDisconnect = []; // audio nodes that will be disconnected when the source has ended @@ -241,12 +271,19 @@ export const superdough = async (value, deadline, hapDuration) => { reverbSend = effectSend(post, reverbNode, room); } + // analyser + let analyserSend; + if (analyze) { + const analyserNode = getAnalyser(/* orbit, */ 2 ** (fft + 5)); + analyserSend = effectSend(post, analyserNode, analyze); + } + // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); // toDisconnect = all the node that should be disconnected in onended callback // this is crucial for performance - toDisconnect = chain.concat([delaySend, reverbSend]); + toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]); }; export const superdoughTrigger = (t, hap, ct, cps) => superdough(hap, t - ct, hap.duration / cps, cps); From 68ea086e2aec4360652e8f84202979d5007137f8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 25 Aug 2023 12:39:38 +0200 Subject: [PATCH 003/161] improve canvas quality on retina --- packages/core/draw.mjs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index 8be45e03..d1cdd7be 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -9,18 +9,19 @@ import { Pattern, getTime, State, TimeSpan } from './index.mjs'; export const getDrawContext = (id = 'test-canvas') => { let canvas = document.querySelector('#' + id); if (!canvas) { + const scale = 2; // 2 = crisp on retina screens canvas = document.createElement('canvas'); canvas.id = id; - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; + canvas.width = window.innerWidth * scale; + canvas.height = window.innerHeight * scale; canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0'; document.body.prepend(canvas); let timeout; window.addEventListener('resize', () => { timeout && clearTimeout(timeout); timeout = setTimeout(() => { - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; + canvas.width = window.innerWidth * scale; + canvas.height = window.innerHeight * scale; }, 200); }); } @@ -59,7 +60,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) { export const cleanupDraw = (clearScreen = true) => { const ctx = getDrawContext(); - clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight); + clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width); if (window.strudelAnimation) { cancelAnimationFrame(window.strudelAnimation); } From 965794712e435bcd5d157dfaf6eace8a03a999df Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 25 Aug 2023 12:39:47 +0200 Subject: [PATCH 004/161] fscope + scaling options --- packages/core/scope.mjs | 66 ++++++++++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/packages/core/scope.mjs b/packages/core/scope.mjs index 62b08e62..908ed254 100644 --- a/packages/core/scope.mjs +++ b/packages/core/scope.mjs @@ -2,8 +2,12 @@ import { Pattern } from './pattern.mjs'; import { getDrawContext } from './draw.mjs'; import { analyser } from '@strudel.cycles/webaudio'; -export function drawTimeScope(analyser, dataArray, { align = true, color = 'white', thickness = 2 } = {}) { +export function drawTimeScope( + analyser, + { align = true, color = 'white', thickness = 3, scale = 1, pos = 0.75, next = 1 } = {}, +) { const canvasCtx = getDrawContext(); + const dataArray = getAnalyzerData('time'); canvasCtx.lineWidth = thickness; canvasCtx.strokeStyle = color; @@ -13,16 +17,18 @@ export function drawTimeScope(analyser, dataArray, { align = true, color = 'whit const bufferSize = analyser.frequencyBinCount; const triggerValue = 256 / 2; - const triggerIndex = align + let triggerIndex = align ? Array.from(dataArray).findIndex((v, i, arr) => i && arr[i - 1] < triggerValue && v >= triggerValue) : 0; + triggerIndex = Math.max(triggerIndex, 0); // fallback to 0 when no trigger is found const sliceWidth = (canvas.width * 1.0) / bufferSize; let x = 0; for (let i = triggerIndex; i < bufferSize; i++) { const v = dataArray[i] / 128.0; - const y = (v * (canvas.height / 2)) / 2 + canvas.height / 2; + const y = (scale * (v - 1) + pos) * canvas.height; + if (i === 0) { canvasCtx.moveTo(x, y); } else { @@ -33,16 +39,48 @@ export function drawTimeScope(analyser, dataArray, { align = true, color = 'whit canvasCtx.stroke(); } -Pattern.prototype.scope = function (config = {}) { - return this.analyze(1).draw((ctx) => { - let data = getAnalyzerData('time'); - const { smear = 0 } = config; - if (!smear) { - ctx.clearRect(0, 0, window.innerWidth, window.innerHeight); - } else { - ctx.fillStyle = `rgba(0,0,0,${1 - smear})`; - ctx.fillRect(0, 0, window.innerWidth, window.innerHeight); - } - data && drawTimeScope(analyser, data, config); +export function drawFrequencyScope(analyser, { color = 'white', scale = 1, pos = 0.75, lean = 0.5 } = {}) { + const dataArray = getAnalyzerData('frequency'); + const canvasCtx = getDrawContext(); + const canvas = canvasCtx.canvas; + + canvasCtx.fillStyle = color; + const bufferSize = analyser.frequencyBinCount; + const sliceWidth = (canvas.width * 1.0) / bufferSize; + + let x = 0; + for (let i = 0; i < bufferSize; i++) { + const v = (dataArray[i] / 256.0) * scale; + const h = v * canvas.height; + const y = (pos - v * lean) * canvas.height; + + canvasCtx.fillRect(x, y, Math.max(sliceWidth, 1), h); + x += sliceWidth; + } +} + +function clearScreen(smear = 0, smearRGB = `0,0,0`) { + const canvasCtx = getDrawContext(); + if (!smear) { + canvasCtx.clearRect(0, 0, canvasCtx.canvas.width, canvasCtx.canvas.height); + } else { + canvasCtx.fillStyle = `rgba(${smearRGB},${1 - smear})`; + canvasCtx.fillRect(0, 0, canvasCtx.canvas.width, canvasCtx.canvas.height); + } +} + +Pattern.prototype.fscope = function (config = {}) { + return this.analyze(1).draw(() => { + clearScreen(config.smear); + analyser && drawFrequencyScope(analyser, config); }); }; + +Pattern.prototype.tscope = function (config = {}) { + return this.analyze(1).draw(() => { + clearScreen(config.smear); + analyser && drawTimeScope(analyser, config); + }); +}; + +Pattern.prototype.scope = Pattern.prototype.tscope; From 988bd8ccdc186a03a952364904f72b6dc88d4cf4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 25 Aug 2023 12:40:04 +0200 Subject: [PATCH 005/161] rename stuff --- packages/core/scope.mjs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/core/scope.mjs b/packages/core/scope.mjs index 908ed254..4f4a1d2d 100644 --- a/packages/core/scope.mjs +++ b/packages/core/scope.mjs @@ -6,14 +6,14 @@ export function drawTimeScope( analyser, { align = true, color = 'white', thickness = 3, scale = 1, pos = 0.75, next = 1 } = {}, ) { - const canvasCtx = getDrawContext(); + const ctx = getDrawContext(); const dataArray = getAnalyzerData('time'); - canvasCtx.lineWidth = thickness; - canvasCtx.strokeStyle = color; + ctx.lineWidth = thickness; + ctx.strokeStyle = color; - canvasCtx.beginPath(); - let canvas = canvasCtx.canvas; + ctx.beginPath(); + let canvas = ctx.canvas; const bufferSize = analyser.frequencyBinCount; const triggerValue = 256 / 2; @@ -30,21 +30,21 @@ export function drawTimeScope( const y = (scale * (v - 1) + pos) * canvas.height; if (i === 0) { - canvasCtx.moveTo(x, y); + ctx.moveTo(x, y); } else { - canvasCtx.lineTo(x, y); + ctx.lineTo(x, y); } x += sliceWidth; } - canvasCtx.stroke(); + ctx.stroke(); } export function drawFrequencyScope(analyser, { color = 'white', scale = 1, pos = 0.75, lean = 0.5 } = {}) { const dataArray = getAnalyzerData('frequency'); - const canvasCtx = getDrawContext(); - const canvas = canvasCtx.canvas; + const ctx = getDrawContext(); + const canvas = ctx.canvas; - canvasCtx.fillStyle = color; + ctx.fillStyle = color; const bufferSize = analyser.frequencyBinCount; const sliceWidth = (canvas.width * 1.0) / bufferSize; @@ -54,18 +54,18 @@ export function drawFrequencyScope(analyser, { color = 'white', scale = 1, pos = const h = v * canvas.height; const y = (pos - v * lean) * canvas.height; - canvasCtx.fillRect(x, y, Math.max(sliceWidth, 1), h); + ctx.fillRect(x, y, Math.max(sliceWidth, 1), h); x += sliceWidth; } } function clearScreen(smear = 0, smearRGB = `0,0,0`) { - const canvasCtx = getDrawContext(); + const ctx = getDrawContext(); if (!smear) { - canvasCtx.clearRect(0, 0, canvasCtx.canvas.width, canvasCtx.canvas.height); + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); } else { - canvasCtx.fillStyle = `rgba(${smearRGB},${1 - smear})`; - canvasCtx.fillRect(0, 0, canvasCtx.canvas.width, canvasCtx.canvas.height); + ctx.fillStyle = `rgba(${smearRGB},${1 - smear})`; + ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); } } From 2e31c8695ae9e87bf7f8651c12582b8523aa146c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 25 Aug 2023 16:29:41 +0200 Subject: [PATCH 006/161] use Float32 for higher precision --- packages/core/scope.mjs | 15 ++++++++++----- packages/superdough/superdough.mjs | 10 ++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/core/scope.mjs b/packages/core/scope.mjs index 4f4a1d2d..392ea859 100644 --- a/packages/core/scope.mjs +++ b/packages/core/scope.mjs @@ -1,10 +1,11 @@ import { Pattern } from './pattern.mjs'; import { getDrawContext } from './draw.mjs'; import { analyser } from '@strudel.cycles/webaudio'; +import { clamp } from './util.mjs'; export function drawTimeScope( analyser, - { align = true, color = 'white', thickness = 3, scale = 1, pos = 0.75, next = 1 } = {}, + { align = true, color = 'white', thickness = 3, scale = 0.25, pos = 0.75, next = 1 } = {}, ) { const ctx = getDrawContext(); const dataArray = getAnalyzerData('time'); @@ -16,7 +17,7 @@ export function drawTimeScope( let canvas = ctx.canvas; const bufferSize = analyser.frequencyBinCount; - const triggerValue = 256 / 2; + const triggerValue = 0; let triggerIndex = align ? Array.from(dataArray).findIndex((v, i, arr) => i && arr[i - 1] < triggerValue && v >= triggerValue) : 0; @@ -26,7 +27,7 @@ export function drawTimeScope( let x = 0; for (let i = triggerIndex; i < bufferSize; i++) { - const v = dataArray[i] / 128.0; + const v = dataArray[i] + 1; const y = (scale * (v - 1) + pos) * canvas.height; if (i === 0) { @@ -39,7 +40,10 @@ export function drawTimeScope( ctx.stroke(); } -export function drawFrequencyScope(analyser, { color = 'white', scale = 1, pos = 0.75, lean = 0.5 } = {}) { +export function drawFrequencyScope( + analyser, + { color = 'white', scale = 0.25, pos = 0.75, lean = 0.5, min = -150, max = 0 } = {}, +) { const dataArray = getAnalyzerData('frequency'); const ctx = getDrawContext(); const canvas = ctx.canvas; @@ -50,7 +54,8 @@ export function drawFrequencyScope(analyser, { color = 'white', scale = 1, pos = let x = 0; for (let i = 0; i < bufferSize; i++) { - const v = (dataArray[i] / 256.0) * scale; + const normalized = clamp((dataArray[i] - min) / (max - min), 0, 1); + const v = normalized * scale; const h = v * canvas.height; const y = (pos - v * lean) * canvas.height; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 07b32eec..d5a2ca3f 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -128,19 +128,21 @@ export function getAnalyser(/* orbit, */ fftSize = 2048) { analyserNode.fftSize = fftSize; // getDestination().connect(analyserNode); analyser /* s[orbit] */ = analyserNode; - analyserData = new Uint8Array(analyser.frequencyBinCount); + //analyserData = new Uint8Array(analyser.frequencyBinCount); + analyserData = new Float32Array(analyser.frequencyBinCount); } if (analyser /* s[orbit] */.fftSize !== fftSize) { analyser /* s[orbit] */.fftSize = fftSize; - analyserData = new Uint8Array(analyser.frequencyBinCount); + //analyserData = new Uint8Array(analyser.frequencyBinCount); + analyserData = new Float32Array(analyser.frequencyBinCount); } return analyser /* s[orbit] */; } export function getAnalyzerData(type = 'time') { const getter = { - time: () => analyser?.getByteTimeDomainData(analyserData), - frequency: () => analyser?.getByteFrequencyData(analyserData), + time: () => analyser?.getFloatTimeDomainData(analyserData), + frequency: () => analyser?.getFloatFrequencyData(analyserData), }[type]; if (!getter) { throw new Error(`getAnalyzerData: ${type} not supported. use one of ${Object.keys(getter).join(', ')}`); From 43efe6921d03d6c12db224d435b7baba41b557e8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 26 Aug 2023 21:11:24 +0200 Subject: [PATCH 007/161] move scope to webaudio package --- packages/core/index.mjs | 1 - packages/webaudio/index.mjs | 1 + packages/{core => webaudio}/scope.mjs | 11 ++++------- 3 files changed, 5 insertions(+), 8 deletions(-) rename packages/{core => webaudio}/scope.mjs (89%) diff --git a/packages/core/index.mjs b/packages/core/index.mjs index ceffdcbe..bed63f9a 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -22,7 +22,6 @@ export * from './cyclist.mjs'; export * from './logger.mjs'; export * from './time.mjs'; export * from './draw.mjs'; -export * from './scope.mjs'; export * from './animate.mjs'; export * from './pianoroll.mjs'; export * from './spiral.mjs'; diff --git a/packages/webaudio/index.mjs b/packages/webaudio/index.mjs index 563a367e..a425e683 100644 --- a/packages/webaudio/index.mjs +++ b/packages/webaudio/index.mjs @@ -5,4 +5,5 @@ This program is free software: you can redistribute it and/or modify it under th */ export * from './webaudio.mjs'; +export * from './scope.mjs'; export * from 'superdough'; diff --git a/packages/core/scope.mjs b/packages/webaudio/scope.mjs similarity index 89% rename from packages/core/scope.mjs rename to packages/webaudio/scope.mjs index 392ea859..b3b4b8bd 100644 --- a/packages/core/scope.mjs +++ b/packages/webaudio/scope.mjs @@ -1,11 +1,9 @@ -import { Pattern } from './pattern.mjs'; -import { getDrawContext } from './draw.mjs'; -import { analyser } from '@strudel.cycles/webaudio'; -import { clamp } from './util.mjs'; +import { Pattern, getDrawContext, clamp } from '@strudel.cycles/core'; +import { analyser } from 'superdough'; export function drawTimeScope( analyser, - { align = true, color = 'white', thickness = 3, scale = 0.25, pos = 0.75, next = 1 } = {}, + { align = true, color = 'white', thickness = 3, scale = 0.25, pos = 0.75, next = 1, trigger = 0 } = {}, ) { const ctx = getDrawContext(); const dataArray = getAnalyzerData('time'); @@ -17,9 +15,8 @@ export function drawTimeScope( let canvas = ctx.canvas; const bufferSize = analyser.frequencyBinCount; - const triggerValue = 0; let triggerIndex = align - ? Array.from(dataArray).findIndex((v, i, arr) => i && arr[i - 1] < triggerValue && v >= triggerValue) + ? Array.from(dataArray).findIndex((v, i, arr) => i && arr[i - 1] > -trigger && v <= -trigger) : 0; triggerIndex = Math.max(triggerIndex, 0); // fallback to 0 when no trigger is found From 4dadc18a3a8e9725ac654acdb084b1b68aa5c357 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 26 Aug 2023 21:12:02 +0200 Subject: [PATCH 008/161] fix: import --- packages/webaudio/scope.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webaudio/scope.mjs b/packages/webaudio/scope.mjs index b3b4b8bd..cfde80ce 100644 --- a/packages/webaudio/scope.mjs +++ b/packages/webaudio/scope.mjs @@ -1,5 +1,5 @@ import { Pattern, getDrawContext, clamp } from '@strudel.cycles/core'; -import { analyser } from 'superdough'; +import { analyser, getAnalyzerData } from 'superdough'; export function drawTimeScope( analyser, From a7728e3d81fb7a0a2dff9f2f4bd9e313ddf138cd Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 26 Aug 2023 21:16:46 +0200 Subject: [PATCH 009/161] comment new controls --- packages/core/controls.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index a02a5805..78e517dc 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -146,8 +146,8 @@ const generic_params = [ */ ['bank'], - ['analyze'], // sends - ['fft'], + ['analyze'], // analyser node send amount 0 - 1 (used by scope) + ['fft'], // fftSize of analyser /** * Amplitude envelope decay time: the time it takes after the attack time to reach the sustain level. From e3010907041b91c75433b8d62c01679fcca643a4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 27 Aug 2023 15:27:30 +0200 Subject: [PATCH 010/161] pianoroll improvements: - add label / activeLabel controls - add new pianoroll options: - fill - fillActive - strokeActive - stroke - hideInactive - colorizeInactive - fontFamily - add wordfall method - fix: some haps were drawn with wrong length - pianoroll labels now use set fontFamily - hide fold gutter --- packages/core/controls.mjs | 3 + packages/core/pianoroll.mjs | 131 ++++++++++++------- packages/react/src/hooks/usePatternFrame.mjs | 4 +- packages/react/src/hooks/useStrudel.mjs | 5 +- website/src/repl/Repl.css | 4 + website/src/repl/Repl.jsx | 5 + 6 files changed, 101 insertions(+), 51 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 78e517dc..717a8353 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -506,6 +506,9 @@ const generic_params = [ * */ ['lsize'], + // label for pianoroll + ['activeLabel'], + [['label', 'activeLabel']], // ['lfo'], // ['lfocutoffint'], // ['lfodelay'], diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index ce5020c9..1592ab96 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -191,6 +191,13 @@ export function pianoroll({ fold = 0, vertical = 0, labels = false, + fill, + fillActive = false, + strokeActive = true, + stroke, + hideInactive = 0, + colorizeInactive = 1, + fontFamily, ctx, } = {}) { const w = ctx.canvas.width; @@ -241,51 +248,77 @@ export function pianoroll({ ctx.clearRect(0, 0, w, h); ctx.fillRect(0, 0, w, h); } - /* const inFrame = (event) => - (!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */ - haps - // .filter(inFrame) - .forEach((event) => { - const isActive = event.whole.begin <= time && event.endClipped > time; - const color = event.value?.color || event.context?.color; - ctx.fillStyle = color || inactive; - ctx.strokeStyle = color || active; - ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1; - const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange); - let durationPx = scale(event.duration / timeExtent, 0, timeAxis); - const value = getValue(event); - const valuePx = scale( - fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent, - ...valueRange, - ); - let margin = 0; - const offset = scale(time / timeExtent, ...timeRange); - let coords; - if (vertical) { - coords = [ - valuePx + 1 - (flipValues ? barThickness : 0), // x - timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y - barThickness - 2, // width - durationPx - 2, // height - ]; - } else { - coords = [ - timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x - valuePx + 1 - (flipValues ? 0 : barThickness), // y - durationPx - 2, // widith - barThickness - 2, // height - ]; - } - isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords); - if (labels) { - const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : ''); - ctx.font = `${barThickness * 0.75}px monospace`; - ctx.strokeStyle = 'black'; - ctx.fillStyle = isActive ? 'white' : 'black'; - ctx.textBaseline = 'top'; - ctx.fillText(label, ...coords); - } - }); + haps.forEach((event) => { + const isActive = event.whole.begin <= time && event.endClipped > time; + let strokeCurrent = stroke ?? (strokeActive && isActive); + let fillCurrent = fill ?? (fillActive && isActive); + if (hideInactive && !isActive) { + return; + } + let color = event.value?.color || event.context?.color; + active = color || active; + inactive = colorizeInactive ? color || inactive : inactive; + color = isActive ? active : inactive; + ctx.fillStyle = fillCurrent ? color : 'transparent'; + ctx.strokeStyle = color; + ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1; + const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent; + const timePx = scale(timeProgress, ...timeRange); + let durationPx = scale(event.duration / timeExtent, 0, timeAxis); + const value = getValue(event); + const valueProgress = fold + ? foldValues.indexOf(value) / foldValues.length + : (Number(value) - minMidi) / valueExtent; + const valuePx = scale(valueProgress, ...valueRange); + let margin = 0; + const offset = scale(time / timeExtent, ...timeRange); + let coords; + if (vertical) { + coords = [ + valuePx + 1 - (flipValues ? barThickness : 0), // x + timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y + barThickness - 2, // width + durationPx - 2, // height + ]; + } else { + coords = [ + timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x + valuePx + 1 - (flipValues ? 0 : barThickness), // y + durationPx - 2, // widith + barThickness - 2, // height + ]; + } + /* const xFactor = Math.sin(performance.now() / 500) + 1; + coords[0] *= xFactor; */ + + if (strokeCurrent) { + ctx.strokeRect(...coords); + } + if (fillCurrent) { + ctx.fillRect(...coords); + } + //ctx.ellipse(...ellipseFromRect(...coords)) + if (labels) { + const defaultLabel = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : ''); + const { label: inactiveLabel, activeLabel } = event.value; + const customLabel = isActive ? activeLabel || inactiveLabel : inactiveLabel; + const label = customLabel ?? defaultLabel; + let measure = vertical ? durationPx : barThickness * 0.75; + ctx.font = `${measure}px ${fontFamily}`; + //ctx.strokeStyle = 'white'; + //ctx.lineWidth = 2; + // font color + ctx.fillStyle = /* isActive && */ !fillCurrent ? color : 'black'; + ctx.textBaseline = 'top'; + //ctx.strokeText(label, ...coords); + + /* ctx.translate(coords[0], coords[1]); + ctx.rotate(Math.PI * 4); */ + + ctx.fillText(label, ...coords); + //ctx.setTransform(1, 0, 0, 1, 0, 0); // Sets the identity matrix + } + }); ctx.globalAlpha = 1; // reset! const playheadPosition = scale(-from / timeExtent, ...timeRange); // draw playhead @@ -311,11 +344,15 @@ export function getDrawOptions(drawTime, options = {}) { } Pattern.prototype.punchcard = function (options) { - return this.onPaint((ctx, time, haps, drawTime) => - pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }), + return this.onPaint((ctx, time, haps, drawTime, paintOptions = {}) => + pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) }), ); }; +Pattern.prototype.wordfall = function (options) { + return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...options }); +}; + /* Pattern.prototype.pianoroll = function (options) { return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }), diff --git a/packages/react/src/hooks/usePatternFrame.mjs b/packages/react/src/hooks/usePatternFrame.mjs index 065c6ba7..725fe0a3 100644 --- a/packages/react/src/hooks/usePatternFrame.mjs +++ b/packages/react/src/hooks/usePatternFrame.mjs @@ -25,10 +25,10 @@ function usePatternFrame({ pattern, started, getTime, onDraw, drawTime = [-2, 2] const haps = pattern.queryArc(Math.max(lastFrame.current, phase - 1 / 10), phase); lastFrame.current = phase; visibleHaps.current = (visibleHaps.current || []) - .filter((h) => h.whole.end >= phase - lookbehind - lookahead) // in frame + .filter((h) => h.endClipped >= phase - lookbehind - lookahead) // in frame .concat(haps.filter((h) => h.hasOnset())); onDraw(pattern, phase - lookahead, visibleHaps.current, drawTime); - }, [pattern]), + }, [pattern, onDraw]), ); useEffect(() => { if (started) { diff --git a/packages/react/src/hooks/useStrudel.mjs b/packages/react/src/hooks/useStrudel.mjs index 223c21ba..a10998e7 100644 --- a/packages/react/src/hooks/useStrudel.mjs +++ b/packages/react/src/hooks/useStrudel.mjs @@ -18,6 +18,7 @@ function useStrudel({ canvasId, drawContext, drawTime = [-2, 2], + paintOptions = {}, }) { const id = useMemo(() => s4(), []); canvasId = canvasId || `canvas-${id}`; @@ -85,9 +86,9 @@ function useStrudel({ (pattern, time, haps, drawTime) => { const { onPaint } = pattern.context || {}; const ctx = typeof drawContext === 'function' ? drawContext(canvasId) : drawContext; - onPaint?.(ctx, time, haps, drawTime); + onPaint?.(ctx, time, haps, drawTime, paintOptions); }, - [drawContext, canvasId], + [drawContext, canvasId, paintOptions], ); const drawFirstFrame = useCallback( diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index f7227d7d..0400db7a 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -53,3 +53,7 @@ #code .cm-cursor { border-left: 2px solid currentcolor !important; } + +#code .cm-foldGutter { + display: none !important; +} diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index aa83317d..3674c581 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -50,6 +50,7 @@ const modules = [ import('@strudel.cycles/serial'), import('@strudel.cycles/soundfonts'), import('@strudel.cycles/csound'), + import('@strudel.cycles/emoji'), ]; const modulesLoading = evalScope( @@ -125,6 +126,8 @@ export function Repl({ embedded = false }) { panelPosition, } = useSettings(); + const paintOptions = useMemo(() => ({ fontFamily }), [fontFamily]); + const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } = useStrudel({ initialCode: '// LOADING...', @@ -147,6 +150,8 @@ export function Repl({ embedded = false }) { }, onToggle: (play) => !play && cleanupDraw(false), drawContext, + // drawTime: [0, 6], + paintOptions, }); // init code From 88651149d3d1b74b4873dcaf6c388e04eeb46a23 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 27 Aug 2023 15:28:46 +0200 Subject: [PATCH 011/161] fix: don't import emoji pkg --- website/src/repl/Repl.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 3674c581..4bca419a 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -50,7 +50,6 @@ const modules = [ import('@strudel.cycles/serial'), import('@strudel.cycles/soundfonts'), import('@strudel.cycles/csound'), - import('@strudel.cycles/emoji'), ]; const modulesLoading = evalScope( From 6f6def34f7ebc35dd3a21594835049a986dd600e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 27 Aug 2023 15:35:44 +0200 Subject: [PATCH 012/161] fix: improve performance of setting patterning --- website/src/settings.mjs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 036c5509..5608b894 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -38,13 +38,15 @@ export const setLatestCode = (code) => settingsMap.setKey('latestCode', code); export const setIsZen = (active) => settingsMap.setKey('isZen', !!active); const patternSetting = (key) => - register(key, (value, pat) => { - value = Array.isArray(value) ? value.join(' ') : value; - if (value !== settingsMap.get()[key]) { - settingsMap.setKey(key, value); - } - return pat; - }); + register(key, (value, pat) => + pat.onTrigger(() => { + value = Array.isArray(value) ? value.join(' ') : value; + if (value !== settingsMap.get()[key]) { + settingsMap.setKey(key, value); + } + return pat; + }, false), + ); export const theme = patternSetting('theme'); export const fontFamily = patternSetting('fontFamily'); From 038e6c312b9aeef74893812f4dbae322a5e1f619 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 27 Aug 2023 16:05:58 +0200 Subject: [PATCH 013/161] dedupe .pianoroll --- packages/core/pianoroll.mjs | 145 +++--------------------------------- 1 file changed, 12 insertions(+), 133 deletions(-) diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index 1592ab96..f3d2c38a 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -29,138 +29,26 @@ const getValue = (e) => { return value; }; -Pattern.prototype.pianoroll = function ({ - cycles = 4, - playhead = 0.5, - overscan = 1, - flipTime = 0, - flipValues = 0, - hideNegative = false, - // inactive = '#C9E597', - // inactive = '#FFCA28', - inactive = '#7491D2', - active = '#FFCA28', - // background = '#2A3236', - background = 'transparent', - smear = 0, - playheadColor = 'white', - minMidi = 10, - maxMidi = 90, - autorange = 0, - timeframe: timeframeProp, - fold = 0, - vertical = 0, - labels = 0, -} = {}) { - const ctx = getDrawContext(); - const w = ctx.canvas.width; - const h = ctx.canvas.height; +Pattern.prototype.pianoroll = function (options = {}) { + let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options; + let from = -cycles * playhead; let to = cycles * (1 - playhead); - if (timeframeProp) { - console.warn('timeframe is deprecated! use from/to instead'); - from = 0; - to = timeframeProp; - } - const timeAxis = vertical ? h : w; - const valueAxis = vertical ? w : h; - let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time - const timeExtent = to - from; // number of seconds that fit inside the canvas frame - const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values - let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true - let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true - let foldValues = []; - flipTime && timeRange.reverse(); - flipValues && valueRange.reverse(); - this.draw( - (ctx, events, t) => { - ctx.fillStyle = background; - ctx.globalAlpha = 1; // reset! - if (!smear) { - ctx.clearRect(0, 0, w, h); - ctx.fillRect(0, 0, w, h); - } + (ctx, haps, t) => { const inFrame = (event) => (!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from; - events.filter(inFrame).forEach((event) => { - const isActive = event.whole.begin <= t && event.endClipped > t; - ctx.fillStyle = event.context?.color || inactive; - ctx.strokeStyle = event.context?.color || active; - ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1; - const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange); - let durationPx = scale(event.duration / timeExtent, 0, timeAxis); - const value = getValue(event); - const valuePx = scale( - fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent, - ...valueRange, - ); - let margin = 0; - const offset = scale(t / timeExtent, ...timeRange); - let coords; - if (vertical) { - coords = [ - valuePx + 1 - (flipValues ? barThickness : 0), // x - timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y - barThickness - 2, // width - durationPx - 2, // height - ]; - } else { - coords = [ - timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x - valuePx + 1 - (flipValues ? 0 : barThickness), // y - durationPx - 2, // widith - barThickness - 2, // height - ]; - } - isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords); - if (labels) { - const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : ''); - ctx.font = `${barThickness * 0.75}px monospace`; - ctx.strokeStyle = 'black'; - ctx.fillStyle = isActive ? 'white' : 'black'; - ctx.textBaseline = 'top'; - ctx.fillText(label, ...coords); - } + pianoroll({ + ...options, + time: t, + ctx, + haps: haps.filter(inFrame), }); - ctx.globalAlpha = 1; // reset! - const playheadPosition = scale(-from / timeExtent, ...timeRange); - // draw playhead - ctx.strokeStyle = playheadColor; - ctx.beginPath(); - if (vertical) { - ctx.moveTo(0, playheadPosition); - ctx.lineTo(valueAxis, playheadPosition); - } else { - ctx.moveTo(playheadPosition, 0); - ctx.lineTo(playheadPosition, valueAxis); - } - ctx.stroke(); }, { from: from - overscan, to: to + overscan, - onQuery: (events) => { - const { min, max, values } = events.reduce( - ({ min, max, values }, e) => { - const v = getValue(e); - return { - min: v < min ? v : min, - max: v > max ? v : max, - values: values.includes(v) ? values : [...values, v], - }; - }, - { min: Infinity, max: -Infinity, values: [] }, - ); - if (autorange) { - minMidi = min; - maxMidi = max; - valueExtent = maxMidi - minMidi + 1; - } - foldValues = values.sort((a, b) => String(a).localeCompare(String(b))); - barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent; - }, }, ); return this; @@ -191,7 +79,7 @@ export function pianoroll({ fold = 0, vertical = 0, labels = false, - fill, + fill = 1, fillActive = false, strokeActive = true, stroke, @@ -241,7 +129,6 @@ export function pianoroll({ // foldValues = values.sort((a, b) => a - b); foldValues = values.sort((a, b) => String(a).localeCompare(String(b))); barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent; - ctx.fillStyle = background; ctx.globalAlpha = 1; // reset! if (!smear) { @@ -251,7 +138,7 @@ export function pianoroll({ haps.forEach((event) => { const isActive = event.whole.begin <= time && event.endClipped > time; let strokeCurrent = stroke ?? (strokeActive && isActive); - let fillCurrent = fill ?? (fillActive && isActive); + let fillCurrent = (!isActive && fill) || (isActive && fillActive); if (hideInactive && !isActive) { return; } @@ -304,19 +191,11 @@ export function pianoroll({ const customLabel = isActive ? activeLabel || inactiveLabel : inactiveLabel; const label = customLabel ?? defaultLabel; let measure = vertical ? durationPx : barThickness * 0.75; - ctx.font = `${measure}px ${fontFamily}`; - //ctx.strokeStyle = 'white'; - //ctx.lineWidth = 2; + ctx.font = `${measure}px ${fontFamily || 'monospace'}`; // font color ctx.fillStyle = /* isActive && */ !fillCurrent ? color : 'black'; ctx.textBaseline = 'top'; - //ctx.strokeText(label, ...coords); - - /* ctx.translate(coords[0], coords[1]); - ctx.rotate(Math.PI * 4); */ - ctx.fillText(label, ...coords); - //ctx.setTransform(1, 0, 0, 1, 0, 0); // Sets the identity matrix } }); ctx.globalAlpha = 1; // reset! From f6cf7405074d562c67960af6d21f1725ae879d1c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 27 Aug 2023 22:11:22 +0200 Subject: [PATCH 014/161] add emoji support --- website/src/repl/Repl.jsx | 17 +++++------------ website/src/repl/helpers.mjs | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 12 deletions(-) create mode 100644 website/src/repl/helpers.mjs diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 4bca419a..8dafe8a4 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -5,15 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger } from '@strudel.cycles/core'; -import { - CodeMirror, - cx, - flash, - useHighlighting, - useStrudel, - useKeydown, - updateMiniLocations, -} from '@strudel.cycles/react'; +import { CodeMirror, cx, flash, useHighlighting, useStrudel, useKeydown } from '@strudel.cycles/react'; import { getAudioContext, initAudioOnFirstClick, resetLoadedSounds, webaudioOutput } from '@strudel.cycles/webaudio'; import { createClient } from '@supabase/supabase-js'; import { nanoid } from 'nanoid'; @@ -28,6 +20,7 @@ import { themes } from './themes.mjs'; import { settingsMap, useSettings, setLatestCode } from '../settings.mjs'; import Loader from './Loader'; import { settingPatterns } from '../settings.mjs'; +import { code2hash, hash2code } from './helpers.mjs'; const { latestCode } = settingsMap.get(); @@ -73,11 +66,11 @@ async function initCode() { try { const initialUrl = window.location.href; const hash = initialUrl.split('?')[1]?.split('#')?.[0]; - const codeParam = window.location.href.split('#')[1]; + const codeParam = window.location.href.split('#')[1] || ''; // looking like https://strudel.tidalcycles.org/?J01s5i1J0200 (fixed hash length) if (codeParam) { // looking like https://strudel.tidalcycles.org/#ImMzIGUzIg%3D%3D (hash length depends on code length) - return atob(decodeURIComponent(codeParam || '')); + return hash2code(codeParam); } else if (hash) { return supabase .from('code') @@ -142,7 +135,7 @@ export function Repl({ embedded = false }) { setMiniLocations(meta.miniLocations); setPending(false); setLatestCode(code); - window.location.hash = '#' + encodeURIComponent(btoa(code)); + window.location.hash = '#' + code2hash(code); }, onEvalError: (err) => { setPending(false); diff --git a/website/src/repl/helpers.mjs b/website/src/repl/helpers.mjs new file mode 100644 index 00000000..b86e76f1 --- /dev/null +++ b/website/src/repl/helpers.mjs @@ -0,0 +1,25 @@ +export function unicodeToBase64(text) { + const utf8Bytes = new TextEncoder().encode(text); + const base64String = btoa(String.fromCharCode(...utf8Bytes)); + return base64String; +} + +export function base64ToUnicode(base64String) { + const utf8Bytes = new Uint8Array( + atob(base64String) + .split('') + .map((char) => char.charCodeAt(0)), + ); + const decodedText = new TextDecoder().decode(utf8Bytes); + return decodedText; +} + +export function code2hash(code) { + return encodeURIComponent(unicodeToBase64(code)); + //return '#' + encodeURIComponent(btoa(code)); +} + +export function hash2code(hash) { + return base64ToUnicode(decodeURIComponent(hash)); + //return atob(decodeURIComponent(codeParam || '')); +} From 1faa81099c8fd3a7c6a2efbed47955c2239b0ef6 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 28 Aug 2023 14:50:06 +0200 Subject: [PATCH 015/161] add protracker3 font --- website/public/fonts/protracker/license.txt | 4 ++++ .../fonts/protracker/protracker3-font.ttf | Bin 0 -> 13488 bytes website/public/fonts/protracker/readme.txt | 16 ++++++++++++++++ website/src/repl/Footer.jsx | 1 + website/src/styles/index.css | 4 ++++ 5 files changed, 25 insertions(+) create mode 100644 website/public/fonts/protracker/license.txt create mode 100644 website/public/fonts/protracker/protracker3-font.ttf create mode 100644 website/public/fonts/protracker/readme.txt diff --git a/website/public/fonts/protracker/license.txt b/website/public/fonts/protracker/license.txt new file mode 100644 index 00000000..7c555087 --- /dev/null +++ b/website/public/fonts/protracker/license.txt @@ -0,0 +1,4 @@ +The FontStruction “Protracker3 Font” +(https://fontstruct.com/fontstructions/show/1952645) by “pznfbg” is licensed +under a Creative Commons Attribution Share Alike license +(http://creativecommons.org/licenses/by-sa/3.0/). diff --git a/website/public/fonts/protracker/protracker3-font.ttf b/website/public/fonts/protracker/protracker3-font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..661ead4a4c3599356c1a11880df98f19b52b067f GIT binary patch literal 13488 zcmeHNU2I&%6+ZiGJ8=>qBr*Bp(x#Ns#OpW?C5Tjs+=R9dG>RiY6$aM!+TO(WZg#J6 z>>@#{79rt>O638hJ|NNu)FQQ&kcvXprV@QiMWIxp6;hLmN^zob#QZIWsnvgovz_CnP0ZU+eqEJ$Qqp=S7UI-|EYChd(_r0MVUz ze`IhxUzSa&mqk*oki9lCHa+~^-=F`ANcND(=T3|k@{pSpRpK#?Oc#ZEc{_Xq~(Y~8dzkREh3pMiO`PZ>*yTbL3Q6LTWU?C4)<%dO$CNE{e3D0KF`-Zae5P(1#-HM?sKn z0GthnK+l8T7rE|E5cFSvBM9>AA-_RDkAPkg*?0nU9_82r8UsPLsRMLIWHZ)m4neTL zB>+7Gx**cA8*~ix6bRpLg07q164{D5TcKm?OCsCaK@Wg_0{U2F`x79@cR;>l40J(c zCw$*|UgYN8pyxqvqwpaMz7Kj=bmUyFOI}U@#r)HA9X=}M)jN)co_3@28Zve+Nxk6w+_p4gq9vLu z5806@Xv;^g9{HXmLe83>O}=NxPtPhsqE|i+rNw9BnL&-LFrpeUz;fi9=nYy78Y7~ddoz0X z=W!0_>vubAepaJTPGu$=u67_ZHI$g-cV@wkx(=t-IYt6C;_V=fBj*xTy1!mKL355- zw-zt{`*6&>fgIT45x$+i(ssBrij^o1b>i)YwqTNSSHO5 zwP$BzM{r$_k<)r0A61&k^{l;xK$kgB%^BFtYqwX_Q{5}7U8^CouuNw_@HjW!pLqjS z;h!R=LhSi$`=HIgW^;M0H7tmN;xpd6sHp7fu-57}W`|R|MQKM?d|eN7Uu`$>XS+QY z?X}oWMUOP{TJdvlAW)E-IyC*5dik3Vm2&567PQvz)!qS8aX%ILwM{K*OM6jN)5u!Q zW1cFuHXHVLvs!YUN+U)O>Y|1F4!g3$sbbRjNQXA*T_W8}wApbl#~ZMMo_3F{&7Tp%(Sr-L#^-axXS{VB7etm>H?s zX-Yrb{Xgnk%o;H$9#aFbt`XzAe&amPw+vp)qgJD8T7)?+|6Jx`OACf~_)HY*DoQal zXjdUuSdaXProkV|h{j~f#FiOvtQ6nRWzLP* z6RMftTDwHBwExu5P^-;)Ih--Gy_=WzqWF8Yo`TFMkr+Z%DevtM?}+}9C4#^&0vauu=Crq!XH$o!5SHJiL_d~8y)b(r8g->Hwdo8Go-r;5e`DDaah zOO_~zz+#~$h;eIcjI;jkK;vS2n|fo`Mg4sBu3bBC7$M!`HpZH7#*D-ntktHmZ0NLF z?Of}hxT|+<)-EeK?svYFn_RejXxCi5YoiL$Wwq}`@z7{#ood9iX8FD0ELgp3yOXFE zO?Nu2-nF&6t=_fWMG?D)JBg4%o4tf*+v;80_ElCzLw{{@e`WRf_rm|dU0Y;d z@}$wre{QdF2?=U%Nm(!7bYD`CKj8SZbjcaVXJoUy==iMcl;1eMMedV#9N#Lt6Wxw) zllBDvB7)Bv`F27vtd%DduUWCQOF5}_*U7OYZ^3-l%Y&&kmc{=wI-VM~Jgx*CsaNdH z0Fq6qKRP}k*QNg9_@uO?K5~2t@+*!{OGmoh@%XEW^e)F|WkWjW_!jwc`m2s_mCfm* z0dg&UG7YO==gPVTV}W8*Gp&SgyT2Lmone8JbY=-{0w*Z zL>j;Al-o1Eay;^qx#W1{C7WJ|jz?ay?>QcM zY1!m>5Dp1l|06d+*6|Y4R>DXsbPXaZ)D@qmr=0Vn%A!r^qm`_1gg5Qx({>o0Q zFJUHQ6e31dxl3~RZ`k=?HTtX-o$Ez1*xm$f&04SnmZyM(_y!QkB4SCzQ{Y0_t<4l4 z*P5$B6pz|YAs-eWEd;%# z@o`uQ4u@f-I4~J9IDMn}N+CErRy!Azk%I$fy-CrfaDd>XMh6AToF4o@AvcWCc{k^83(9$anP gDE!s7z1p@5wZrc--L`pE#&3kpa{(u6{F9*gKl$wQS^xk5 literal 0 HcmV?d00001 diff --git a/website/public/fonts/protracker/readme.txt b/website/public/fonts/protracker/readme.txt new file mode 100644 index 00000000..9078a43f --- /dev/null +++ b/website/public/fonts/protracker/readme.txt @@ -0,0 +1,16 @@ +The font file in this archive was created using Fontstruct the free, online +font-building tool. +This font was created by “pznfbg”. +This font has a homepage where this archive and other versions may be found: +https://fontstruct.com/fontstructions/show/1952645 + +Try Fontstruct at https://fontstruct.com +It’s easy and it’s fun. + +Fontstruct is copyright ©2021-2023 Rob Meek + +LEGAL NOTICE: +In using this font you must comply with the licensing terms described in the +file “license.txt” included with this archive. +If you redistribute the font file in this archive, it must be accompanied by all +the other files from this archive, including this one. diff --git a/website/src/repl/Footer.jsx b/website/src/repl/Footer.jsx index 77a3efbc..afacc42b 100644 --- a/website/src/repl/Footer.jsx +++ b/website/src/repl/Footer.jsx @@ -375,6 +375,7 @@ const fontFamilyOptions = { 'we-come-in-peace': 'we-come-in-peace', FiraCode: 'FiraCode', 'FiraCode-SemiBold': 'FiraCode SemiBold', + 'protracker3': 'protracker3', }; function SettingsTab({ scheduler }) { diff --git a/website/src/styles/index.css b/website/src/styles/index.css index 2a54b708..1477e189 100644 --- a/website/src/styles/index.css +++ b/website/src/styles/index.css @@ -26,6 +26,10 @@ font-family: 'FiraCode-SemiBold'; src: url('/fonts/FiraCode/FiraCode-SemiBold.ttf'); } +@font-face { + font-family: 'protracker3'; + src: url('/fonts/protracker/protracker3-font.ttf'); +} .prose > h1:not(:first-child) { margin-top: 30px; From cb643f8fd9cd23842f469def3b1cfd9f3d2d7c1a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 28 Aug 2023 18:54:33 +0200 Subject: [PATCH 016/161] add protracker theme + fix: make sure all text has a bg --- packages/react/src/index.js | 1 + packages/react/src/themes/algoboy.js | 1 + packages/react/src/themes/blackscreen.js | 1 + packages/react/src/themes/bluescreen.js | 1 + packages/react/src/themes/protracker.js | 50 ++++++++++++++++++++++ packages/react/src/themes/strudel-theme.js | 1 + website/src/repl/themes.mjs | 3 ++ 7 files changed, 58 insertions(+) create mode 100644 packages/react/src/themes/protracker.js diff --git a/packages/react/src/index.js b/packages/react/src/index.js index f5bdbb35..b2195134 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -8,4 +8,5 @@ export { default as usePostMessage } from './hooks/usePostMessage'; export { default as useKeydown } from './hooks/useKeydown'; export { default as useEvent } from './hooks/useEvent'; export { default as strudelTheme } from './themes/strudel-theme'; +export { default as protracker } from './themes/protracker'; export { default as cx } from './cx'; diff --git a/packages/react/src/themes/algoboy.js b/packages/react/src/themes/algoboy.js index b4db09de..38cba0fc 100644 --- a/packages/react/src/themes/algoboy.js +++ b/packages/react/src/themes/algoboy.js @@ -35,5 +35,6 @@ export default createTheme({ { tag: t.propertyName, color: '#0f380f' }, { tag: t.className, color: '#0f380f' }, { tag: t.invalid, color: '#0f380f' }, + { tag: [t.unit, t.punctuation], color: '#0f380f' }, ], }); diff --git a/packages/react/src/themes/blackscreen.js b/packages/react/src/themes/blackscreen.js index ac9627c3..135285a3 100644 --- a/packages/react/src/themes/blackscreen.js +++ b/packages/react/src/themes/blackscreen.js @@ -33,5 +33,6 @@ export default createTheme({ { tag: t.propertyName, color: 'white' }, { tag: t.className, color: 'white' }, { tag: t.invalid, color: 'white' }, + { tag: [t.unit, t.punctuation], color: 'white' }, ], }); diff --git a/packages/react/src/themes/bluescreen.js b/packages/react/src/themes/bluescreen.js index 4f72d8c5..aa6489d6 100644 --- a/packages/react/src/themes/bluescreen.js +++ b/packages/react/src/themes/bluescreen.js @@ -36,5 +36,6 @@ export default createTheme({ { tag: t.propertyName, color: 'white' }, { tag: t.className, color: 'white' }, { tag: t.invalid, color: 'white' }, + { tag: [t.unit, t.punctuation], color: 'white' }, ], }); diff --git a/packages/react/src/themes/protracker.js b/packages/react/src/themes/protracker.js new file mode 100644 index 00000000..c1bd761e --- /dev/null +++ b/packages/react/src/themes/protracker.js @@ -0,0 +1,50 @@ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from '@uiw/codemirror-themes'; + +let colorA = '#3f51e6'; +let colorB = '#f9db4a'; +let colorC = '#9f2822'; +let colorD = '#959595'; + +export const settings = { + background: '#00000f', + foreground: colorB, // whats that? + caret: colorC, + selection: colorD, + selectionMatch: colorA, + lineHighlight: '#22222280', // panel bg + lineBackground: '#00000090', + gutterBackground: 'transparent', + gutterForeground: '#8a919966', +}; + +//let punctuation = colorA; +let keywords = colorB; +let punctuation = colorD; +let mini = colorB; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { tag: t.keyword, color: colorA }, + { tag: t.operator, color: mini }, + { tag: t.special(t.variableName), color: colorA }, + { tag: t.typeName, color: colorA }, + { tag: t.atom, color: colorA }, + { tag: t.number, color: mini }, + { tag: t.definition(t.variableName), color: colorA }, + { tag: t.string, color: mini }, + { tag: t.special(t.string), color: mini }, + { tag: t.comment, color: punctuation }, + { tag: t.variableName, color: colorA }, + { tag: t.tagName, color: colorA }, + { tag: t.bracket, color: punctuation }, + { tag: t.meta, color: colorA }, + { tag: t.attributeName, color: colorA }, + { tag: t.propertyName, color: colorA }, // methods + { tag: t.className, color: colorA }, + { tag: t.invalid, color: colorC }, + { tag: [t.unit, t.punctuation], color: punctuation }, + ], +}); diff --git a/packages/react/src/themes/strudel-theme.js b/packages/react/src/themes/strudel-theme.js index 36987c33..4ae31060 100644 --- a/packages/react/src/themes/strudel-theme.js +++ b/packages/react/src/themes/strudel-theme.js @@ -40,5 +40,6 @@ export default createTheme({ { tag: t.className, color: '#decb6b' }, { tag: t.invalid, color: '#ffffff' }, + { tag: [t.unit, t.punctuation], color: '#82aaff' }, ], }); diff --git a/website/src/repl/themes.mjs b/website/src/repl/themes.mjs index af70c9eb..6d6219cc 100644 --- a/website/src/repl/themes.mjs +++ b/website/src/repl/themes.mjs @@ -34,6 +34,7 @@ import strudelTheme from '@strudel.cycles/react/src/themes/strudel-theme'; import bluescreen, { settings as bluescreenSettings } from '@strudel.cycles/react/src/themes/bluescreen'; import blackscreen, { settings as blackscreenSettings } from '@strudel.cycles/react/src/themes/blackscreen'; import whitescreen, { settings as whitescreenSettings } from '@strudel.cycles/react/src/themes/whitescreen'; +import protracker, { settings as protrackerSettings } from '@strudel.cycles/react/src/themes/protracker'; import algoboy, { settings as algoboySettings } from '@strudel.cycles/react/src/themes/algoboy'; import terminal, { settings as terminalSettings } from '@strudel.cycles/react/src/themes/terminal'; @@ -42,6 +43,7 @@ export const themes = { bluescreen, blackscreen, whitescreen, + protracker, algoboy, terminal, abcdef, @@ -95,6 +97,7 @@ export const settings = { bluescreen: bluescreenSettings, blackscreen: blackscreenSettings, whitescreen: whitescreenSettings, + protracker: protrackerSettings, algoboy: algoboySettings, terminal: terminalSettings, abcdef: { From 0e3d84e5ca493f79573a3d8c665bb8aef772d1e0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 28 Aug 2023 19:21:17 +0200 Subject: [PATCH 017/161] add teletext theme + font --- packages/react/src/index.js | 1 + packages/react/src/themes/teletext.js | 49 +++++++ .../fonts/teletext/EuropeanTeletext.ttf | Bin 0 -> 24216 bytes .../fonts/teletext/EuropeanTeletextNuevo.ttf | Bin 0 -> 24736 bytes website/public/fonts/teletext/LICENSE.txt | 121 ++++++++++++++++++ website/src/repl/Footer.jsx | 3 +- website/src/repl/themes.mjs | 3 + website/src/styles/index.css | 4 + 8 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 packages/react/src/themes/teletext.js create mode 100644 website/public/fonts/teletext/EuropeanTeletext.ttf create mode 100644 website/public/fonts/teletext/EuropeanTeletextNuevo.ttf create mode 100644 website/public/fonts/teletext/LICENSE.txt diff --git a/packages/react/src/index.js b/packages/react/src/index.js index b2195134..84b31283 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -9,4 +9,5 @@ export { default as useKeydown } from './hooks/useKeydown'; export { default as useEvent } from './hooks/useEvent'; export { default as strudelTheme } from './themes/strudel-theme'; export { default as protracker } from './themes/protracker'; +export { default as teletext } from './themes/teletext'; export { default as cx } from './cx'; diff --git a/packages/react/src/themes/teletext.js b/packages/react/src/themes/teletext.js new file mode 100644 index 00000000..8e709ba4 --- /dev/null +++ b/packages/react/src/themes/teletext.js @@ -0,0 +1,49 @@ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from '@uiw/codemirror-themes'; + +let colorA = '#6edee4'; +//let colorB = 'magenta'; +let colorB = 'white'; +let colorC = 'red'; +let colorD = '#f8fc55'; + +export const settings = { + background: '#00000f', + foreground: colorA, // whats that? + caret: colorC, + selection: colorD, + selectionMatch: colorA, + lineHighlight: '#00000090', // panel bg + lineBackground: '#00000080', + gutterBackground: 'transparent', + gutterForeground: '#8a919966', +}; + +let punctuation = colorD; +let mini = colorB; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { tag: t.keyword, color: colorA }, + { tag: t.operator, color: mini }, + { tag: t.special(t.variableName), color: colorA }, + { tag: t.typeName, color: colorA }, + { tag: t.atom, color: colorA }, + { tag: t.number, color: mini }, + { tag: t.definition(t.variableName), color: colorA }, + { tag: t.string, color: mini }, + { tag: t.special(t.string), color: mini }, + { tag: t.comment, color: punctuation }, + { tag: t.variableName, color: colorA }, + { tag: t.tagName, color: colorA }, + { tag: t.bracket, color: punctuation }, + { tag: t.meta, color: colorA }, + { tag: t.attributeName, color: colorA }, + { tag: t.propertyName, color: colorA }, // methods + { tag: t.className, color: colorA }, + { tag: t.invalid, color: colorC }, + { tag: [t.unit, t.punctuation], color: punctuation }, + ], +}); diff --git a/website/public/fonts/teletext/EuropeanTeletext.ttf b/website/public/fonts/teletext/EuropeanTeletext.ttf new file mode 100644 index 0000000000000000000000000000000000000000..64391841875f6832ef3b70ab10858d1997b0100e GIT binary patch literal 24216 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

guM~q7;8F1Ci!j}*D6md(y=ym#Zj^CgEdi> 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 literal 0 HcmV?d00001 diff --git a/website/public/fonts/teletext/EuropeanTeletextNuevo.ttf b/website/public/fonts/teletext/EuropeanTeletextNuevo.ttf new file mode 100644 index 0000000000000000000000000000000000000000..99ea3853ee23d05c0f2e80c068c493600db9c35e GIT binary patch literal 24736 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 h1:not(:first-child) { margin-top: 30px; From 289376840e9f9f56643a518437a35bc3db230ccd Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 28 Aug 2023 23:52:24 +0200 Subject: [PATCH 018/161] add mode7 font + can now inject custom theme styles --- packages/react/src/themes/algoboy.js | 1 + packages/react/src/themes/teletext.js | 3 ++- website/public/fonts/mode7/MODE7GX3.TTF | Bin 0 -> 62312 bytes website/src/components/HeadCommon.astro | 9 ++++++++- website/src/repl/Footer.jsx | 1 + website/src/repl/themes.mjs | 8 ++++++++ website/src/styles/index.css | 4 ++++ 7 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 website/public/fonts/mode7/MODE7GX3.TTF diff --git a/packages/react/src/themes/algoboy.js b/packages/react/src/themes/algoboy.js index 38cba0fc..399370e1 100644 --- a/packages/react/src/themes/algoboy.js +++ b/packages/react/src/themes/algoboy.js @@ -12,6 +12,7 @@ export const settings = { gutterBackground: 'transparent', gutterForeground: '#0f380f', light: true, + customStyle: '.cm-line { line-height: 1 }', }; export default createTheme({ theme: 'light', diff --git a/packages/react/src/themes/teletext.js b/packages/react/src/themes/teletext.js index 8e709ba4..d116124a 100644 --- a/packages/react/src/themes/teletext.js +++ b/packages/react/src/themes/teletext.js @@ -8,7 +8,7 @@ let colorC = 'red'; let colorD = '#f8fc55'; export const settings = { - background: '#00000f', + background: '#000000', foreground: colorA, // whats that? caret: colorC, selection: colorD, @@ -17,6 +17,7 @@ export const settings = { lineBackground: '#00000080', gutterBackground: 'transparent', gutterForeground: '#8a919966', + customStyle: '.cm-line { line-height: 1 }', }; let punctuation = colorD; diff --git a/website/public/fonts/mode7/MODE7GX3.TTF b/website/public/fonts/mode7/MODE7GX3.TTF new file mode 100644 index 0000000000000000000000000000000000000000..c9678aed8f53cc8fa149260f69aadac40fcb4850 GIT binary patch literal 62312 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 literal 0 HcmV?d00001 diff --git a/website/src/components/HeadCommon.astro b/website/src/components/HeadCommon.astro index 23b39842..2aa6acfe 100644 --- a/website/src/components/HeadCommon.astro +++ b/website/src/components/HeadCommon.astro @@ -46,12 +46,14 @@ const base = BASE_URL; {pwaInfo && }