mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
fix(seekbar): pulsewave clean connection, retrotape rolling wheel at playhead
Pulsewave: draw flat played line only up to where the Gaussian envelope
starts (startX), eliminating overlap between the flat line and the wave.
Retrotape: replace dual fixed-position reels with a single spinning reel
that tracks the playhead position across the full seek bar width.
fix(i18n): remove literal 'N' from skip-star descriptions in all languages
Replace 'After N skips' with natural phrasing ('after several skips') in
EN, FR, NL, NB, ZH. Simplify ratingsSkipStarThresholdLabel to a single
word (Skips / Sauts / Overslagen / Hopp / 跳过次数). DE was already fixed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -381,16 +381,6 @@ function drawPulseWave(
|
|||||||
ctx.fillRect(0, cy - 1, buffered * w, 2);
|
ctx.fillRect(0, cy - 1, buffered * w, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Played flat line
|
|
||||||
if (progress > 0) {
|
|
||||||
ctx.globalAlpha = 1;
|
|
||||||
ctx.fillStyle = played;
|
|
||||||
ctx.shadowColor = played;
|
|
||||||
ctx.shadowBlur = 3;
|
|
||||||
ctx.fillRect(0, cy - 1, px, 2);
|
|
||||||
ctx.shadowBlur = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animated pulse centered at playhead
|
// Animated pulse centered at playhead
|
||||||
const pulseR = Math.min(38, w * 0.13);
|
const pulseR = Math.min(38, w * 0.13);
|
||||||
const amp = Math.min(h * 0.42, 5.5);
|
const amp = Math.min(h * 0.42, 5.5);
|
||||||
@@ -398,6 +388,16 @@ function drawPulseWave(
|
|||||||
const startX = Math.max(0, px - pulseR);
|
const startX = Math.max(0, px - pulseR);
|
||||||
const endX = Math.min(w, px + pulseR);
|
const endX = Math.min(w, px + pulseR);
|
||||||
|
|
||||||
|
// Flat played line up to where the wave envelope starts
|
||||||
|
if (progress > 0) {
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
ctx.fillStyle = played;
|
||||||
|
ctx.shadowColor = played;
|
||||||
|
ctx.shadowBlur = 3;
|
||||||
|
ctx.fillRect(0, cy - 1, startX, 2);
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
}
|
||||||
|
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
ctx.strokeStyle = played;
|
ctx.strokeStyle = played;
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = 1.5;
|
||||||
@@ -604,80 +604,76 @@ function drawLiquidFill(
|
|||||||
function drawRetroTape(
|
function drawRetroTape(
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
progress: number,
|
progress: number,
|
||||||
_buffered: number,
|
buffered: number,
|
||||||
animState: AnimState,
|
animState: AnimState,
|
||||||
) {
|
) {
|
||||||
const r = setupCanvas(canvas);
|
const r = setupCanvas(canvas);
|
||||||
if (!r) return;
|
if (!r) return;
|
||||||
const { ctx, w, h } = r;
|
const { ctx, w, h } = r;
|
||||||
const { played, unplayed } = getColors();
|
const { played, buffered: buffCol, unplayed } = getColors();
|
||||||
const cy = h / 2;
|
const cy = h / 2;
|
||||||
|
|
||||||
animState.angle += 0.042;
|
animState.angle += 0.055;
|
||||||
|
|
||||||
const maxR = Math.floor(h / 2) - 1;
|
const reelR = Math.min(h / 2 - 0.5, 9);
|
||||||
const minR = Math.max(2, maxR * 0.28);
|
// Map progress to a center x that keeps the reel fully within the canvas
|
||||||
// supply reel (left): shrinks as progress increases
|
const px = reelR + (w - 2 * reelR) * progress;
|
||||||
const rL = minR + (maxR - minR) * (1 - progress);
|
|
||||||
// takeup reel (right): grows as progress increases
|
|
||||||
const rR = minR + (maxR - minR) * progress;
|
|
||||||
const lx = maxR + 1;
|
|
||||||
const rx = w - maxR - 1;
|
|
||||||
|
|
||||||
// Tape between reels
|
// Background track
|
||||||
const tapeLeft = lx + rL + 0.5;
|
ctx.globalAlpha = 0.3;
|
||||||
const tapeRight = rx - rR - 0.5;
|
ctx.fillStyle = unplayed;
|
||||||
if (tapeRight > tapeLeft) {
|
ctx.fillRect(0, cy - 1, w, 2);
|
||||||
ctx.globalAlpha = 0.45;
|
|
||||||
ctx.fillStyle = unplayed;
|
if (buffered > 0) {
|
||||||
ctx.fillRect(tapeLeft, cy - 1.5, tapeRight - tapeLeft, 3);
|
ctx.globalAlpha = 0.5;
|
||||||
|
ctx.fillStyle = buffCol;
|
||||||
|
ctx.fillRect(0, cy - 1, buffered * w, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const drawReel = (
|
// Played portion — up to the left edge of the reel
|
||||||
cx: number,
|
if (progress > 0) {
|
||||||
radius: number,
|
ctx.globalAlpha = 1;
|
||||||
angle: number,
|
ctx.fillStyle = played;
|
||||||
isRight: boolean,
|
ctx.shadowColor = played;
|
||||||
) => {
|
ctx.shadowBlur = 4;
|
||||||
const color = isRight ? played : unplayed;
|
ctx.fillRect(0, cy - 1, px - reelR, 2);
|
||||||
const alpha = isRight ? (progress > 0 ? 1 : 0.35) : (progress < 1 ? 0.55 : 0.35);
|
|
||||||
const glowing = isRight && progress > 0;
|
|
||||||
|
|
||||||
ctx.globalAlpha = alpha;
|
|
||||||
if (glowing) { ctx.shadowColor = played; ctx.shadowBlur = 6; }
|
|
||||||
|
|
||||||
// Outer ring
|
|
||||||
ctx.strokeStyle = color;
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.shadowBlur = 0;
|
ctx.shadowBlur = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Hub
|
// Spinning reel at playhead
|
||||||
const hubR = Math.max(1.5, radius * 0.3);
|
ctx.globalAlpha = 1;
|
||||||
ctx.fillStyle = color;
|
ctx.strokeStyle = played;
|
||||||
ctx.beginPath();
|
ctx.lineWidth = 1;
|
||||||
ctx.arc(cx, cy, hubR, 0, Math.PI * 2);
|
ctx.shadowColor = played;
|
||||||
ctx.fill();
|
ctx.shadowBlur = 7;
|
||||||
|
|
||||||
// Spokes (only if reel is large enough)
|
// Outer ring
|
||||||
if (radius > hubR + 2.5) {
|
ctx.beginPath();
|
||||||
ctx.lineWidth = 0.9;
|
ctx.arc(px, cy, reelR, 0, Math.PI * 2);
|
||||||
ctx.strokeStyle = color;
|
ctx.stroke();
|
||||||
for (let s = 0; s < 3; s++) {
|
ctx.shadowBlur = 0;
|
||||||
const a = angle + (s * Math.PI * 2) / 3;
|
|
||||||
ctx.beginPath();
|
// Hub
|
||||||
ctx.moveTo(cx + Math.cos(a) * (hubR + 0.5), cy + Math.sin(a) * (hubR + 0.5));
|
const hubR = Math.max(1.5, reelR * 0.28);
|
||||||
ctx.lineTo(cx + Math.cos(a) * (radius - 0.5), cy + Math.sin(a) * (radius - 0.5));
|
ctx.fillStyle = played;
|
||||||
ctx.stroke();
|
ctx.beginPath();
|
||||||
}
|
ctx.arc(px, cy, hubR, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Spokes
|
||||||
|
if (reelR > hubR + 2) {
|
||||||
|
ctx.lineWidth = 0.9;
|
||||||
|
ctx.strokeStyle = played;
|
||||||
|
for (let s = 0; s < 3; s++) {
|
||||||
|
const a = animState.angle + (s * Math.PI * 2) / 3;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(px + Math.cos(a) * (hubR + 0.5), cy + Math.sin(a) * (hubR + 0.5));
|
||||||
|
ctx.lineTo(px + Math.cos(a) * (reelR - 0.5), cy + Math.sin(a) * (reelR - 0.5));
|
||||||
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
drawReel(lx, rL, -animState.angle, false);
|
|
||||||
drawReel(rx, rR, animState.angle, true);
|
|
||||||
|
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -520,13 +520,13 @@ export const deTranslation = {
|
|||||||
tabSystem: 'System',
|
tabSystem: 'System',
|
||||||
tabGeneral: 'Allgemein',
|
tabGeneral: 'Allgemein',
|
||||||
ratingsSectionTitle: 'Bewertungen',
|
ratingsSectionTitle: 'Bewertungen',
|
||||||
ratingsSkipStarTitle: 'Überspringen für 1 Stern',
|
ratingsSkipStarTitle: 'Skip → 1 Stern',
|
||||||
ratingsSkipStarDesc:
|
ratingsSkipStarDesc:
|
||||||
'Bei N Überspringen hintereinander: Titel auf 1 Stern setzen. Nur für zuvor unbewertete Titel.',
|
'Wird ein Titel mehrmals hintereinander übersprungen, bekommt er automatisch 1★. Nur für noch nicht bewertete Titel.',
|
||||||
ratingsSkipStarThresholdLabel: 'Überspringer bis 1★',
|
ratingsSkipStarThresholdLabel: 'Skips',
|
||||||
ratingsMixFilterTitle: 'Filter nach Bewertung',
|
ratingsMixFilterTitle: 'Nach Bewertung filtern',
|
||||||
ratingsMixFilterDesc:
|
ratingsMixFilterDesc:
|
||||||
'Inhalte mit niedriger Bewertung in {{mix}} und {{albums}} filtern. Erneut auf den gewählten Stern klicken, um die Schwelle zu deaktivieren.',
|
'Gering bewertete Titel in {{mix}} und {{albums}} ausblenden. Nochmals auf den gewählten Stern klicken, um den Filter zu deaktivieren.',
|
||||||
ratingsMixMinSong: 'Titel',
|
ratingsMixMinSong: 'Titel',
|
||||||
ratingsMixMinAlbum: 'Alben',
|
ratingsMixMinAlbum: 'Alben',
|
||||||
ratingsMixMinArtist: 'Interpreten',
|
ratingsMixMinArtist: 'Interpreten',
|
||||||
|
|||||||
+2
-2
@@ -507,8 +507,8 @@ export const enTranslation = {
|
|||||||
ratingsSectionTitle: 'Ratings',
|
ratingsSectionTitle: 'Ratings',
|
||||||
ratingsSkipStarTitle: 'Skip for 1 star',
|
ratingsSkipStarTitle: 'Skip for 1 star',
|
||||||
ratingsSkipStarDesc:
|
ratingsSkipStarDesc:
|
||||||
'After N skips in a row, set the track to 1★. Only for tracks not rated before.',
|
'After several skips in a row, set the track to 1★. Only for tracks not yet rated.',
|
||||||
ratingsSkipStarThresholdLabel: 'Skips before 1★',
|
ratingsSkipStarThresholdLabel: 'Skips',
|
||||||
ratingsMixFilterTitle: 'Filter by rating',
|
ratingsMixFilterTitle: 'Filter by rating',
|
||||||
ratingsMixFilterDesc:
|
ratingsMixFilterDesc:
|
||||||
'Filter low-rated items in {{mix}} and {{albums}}. Click the selected star again to turn off the threshold.',
|
'Filter low-rated items in {{mix}} and {{albums}}. Click the selected star again to turn off the threshold.',
|
||||||
|
|||||||
+3
-3
@@ -520,11 +520,11 @@ export const frTranslation = {
|
|||||||
ratingsSectionTitle: 'Notes',
|
ratingsSectionTitle: 'Notes',
|
||||||
ratingsSkipStarTitle: 'Passer pour 1 étoile',
|
ratingsSkipStarTitle: 'Passer pour 1 étoile',
|
||||||
ratingsSkipStarDesc:
|
ratingsSkipStarDesc:
|
||||||
'Après N sauts d’affilée : mettre le morceau à 1 étoile. Uniquement s’il n’était pas encore noté.',
|
"Après plusieurs sauts d’affilée, le morceau passe à 1 étoile. Uniquement s’il n’était pas encore noté.",
|
||||||
ratingsSkipStarThresholdLabel: 'Sauts avant 1★',
|
ratingsSkipStarThresholdLabel: 'Sauts',
|
||||||
ratingsMixFilterTitle: 'Filtrage par note',
|
ratingsMixFilterTitle: 'Filtrage par note',
|
||||||
ratingsMixFilterDesc:
|
ratingsMixFilterDesc:
|
||||||
'Filtrer le contenu peu noté dans {{mix}} et {{albums}}. Cliquer de nouveau sur l’étoile choisie désactive le seuil.',
|
"Filtrer le contenu peu noté dans {{mix}} et {{albums}}. Cliquer de nouveau sur l’étoile choisie désactive le seuil.",
|
||||||
ratingsMixMinSong: 'Morceaux',
|
ratingsMixMinSong: 'Morceaux',
|
||||||
ratingsMixMinAlbum: 'Albums',
|
ratingsMixMinAlbum: 'Albums',
|
||||||
ratingsMixMinArtist: 'Artistes',
|
ratingsMixMinArtist: 'Artistes',
|
||||||
|
|||||||
+2
-2
@@ -503,8 +503,8 @@ export const nbTranslation = {
|
|||||||
ratingsSectionTitle: 'Vurderinger',
|
ratingsSectionTitle: 'Vurderinger',
|
||||||
ratingsSkipStarTitle: 'Hopp for 1 stjerne',
|
ratingsSkipStarTitle: 'Hopp for 1 stjerne',
|
||||||
ratingsSkipStarDesc:
|
ratingsSkipStarDesc:
|
||||||
'Etter N hopp på rad: sett sporet til 1 stjerne. Bare for spor som ikke var vurdert før.',
|
'Etter flere hopp på rad: sett sporet til 1 stjerne. Bare for spor som ikke var vurdert før.',
|
||||||
ratingsSkipStarThresholdLabel: 'Hopp før 1★',
|
ratingsSkipStarThresholdLabel: 'Hopp',
|
||||||
ratingsMixFilterTitle: 'Filtrering etter vurdering',
|
ratingsMixFilterTitle: 'Filtrering etter vurdering',
|
||||||
ratingsMixFilterDesc:
|
ratingsMixFilterDesc:
|
||||||
'Filtrer innhold med lav vurdering i {{mix}} og {{albums}}. Klikk på den valgte stjerna igjen for å slå av terskelen.',
|
'Filtrer innhold med lav vurdering i {{mix}} og {{albums}}. Klikk på den valgte stjerna igjen for å slå av terskelen.',
|
||||||
|
|||||||
+2
-2
@@ -520,8 +520,8 @@ export const nlTranslation = {
|
|||||||
ratingsSectionTitle: 'Beoordelingen',
|
ratingsSectionTitle: 'Beoordelingen',
|
||||||
ratingsSkipStarTitle: 'Overslaan voor 1 ster',
|
ratingsSkipStarTitle: 'Overslaan voor 1 ster',
|
||||||
ratingsSkipStarDesc:
|
ratingsSkipStarDesc:
|
||||||
'Na N overslagen op rij: nummer op 1 ster zetten. Alleen voor nummers die nog niet beoordeeld waren.',
|
'Na meerdere overslagen op rij: nummer op 1 ster zetten. Alleen voor nummers die nog niet beoordeeld waren.',
|
||||||
ratingsSkipStarThresholdLabel: 'Overslagen voor 1★',
|
ratingsSkipStarThresholdLabel: 'Overslagen',
|
||||||
ratingsMixFilterTitle: 'Filter op beoordeling',
|
ratingsMixFilterTitle: 'Filter op beoordeling',
|
||||||
ratingsMixFilterDesc:
|
ratingsMixFilterDesc:
|
||||||
'Content met lage beoordeling filteren in {{mix}} en {{albums}}. Klik opnieuw op de gekozen ster om de drempel uit te zetten.',
|
'Content met lage beoordeling filteren in {{mix}} en {{albums}}. Klik opnieuw op de gekozen ster om de drempel uit te zetten.',
|
||||||
|
|||||||
+2
-2
@@ -500,8 +500,8 @@ export const zhTranslation = {
|
|||||||
ratingsSectionTitle: '评分',
|
ratingsSectionTitle: '评分',
|
||||||
ratingsSkipStarTitle: '跳过以评 1 星',
|
ratingsSkipStarTitle: '跳过以评 1 星',
|
||||||
ratingsSkipStarDesc:
|
ratingsSkipStarDesc:
|
||||||
'连续跳过 N 次后将曲目设为 1 星。仅适用于此前未评分的曲目。',
|
'连续多次跳过后将曲目设为 1 星。仅适用于此前未评分的曲目。',
|
||||||
ratingsSkipStarThresholdLabel: '跳过次数(至 1★)',
|
ratingsSkipStarThresholdLabel: '跳过次数',
|
||||||
ratingsMixFilterTitle: '按评分筛选',
|
ratingsMixFilterTitle: '按评分筛选',
|
||||||
ratingsMixFilterDesc:
|
ratingsMixFilterDesc:
|
||||||
'在{{mix}}与{{albums}}中筛选低评分内容。再次点击所选星标可关闭阈值。',
|
'在{{mix}}与{{albums}}中筛选低评分内容。再次点击所选星标可关闭阈值。',
|
||||||
|
|||||||
Reference in New Issue
Block a user