diff --git a/CMakeLists.txt b/CMakeLists.txt index 218b353..2238e44 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,12 @@ juce_add_plugin(InstaDrums juce_generate_juce_header(InstaDrums) +juce_add_binary_data(InstaDrumsData SOURCES + Resources/Rajdhani-Regular.ttf + Resources/Rajdhani-Medium.ttf + Resources/Rajdhani-Bold.ttf +) + target_sources(InstaDrums PRIVATE Source/PluginProcessor.cpp @@ -42,6 +48,7 @@ target_compile_definitions(InstaDrums target_link_libraries(InstaDrums PRIVATE + InstaDrumsData juce::juce_audio_basics juce::juce_audio_devices juce::juce_audio_formats diff --git a/Resources/Rajdhani-Bold.ttf b/Resources/Rajdhani-Bold.ttf new file mode 100644 index 0000000..47af157 Binary files /dev/null and b/Resources/Rajdhani-Bold.ttf differ diff --git a/Resources/Rajdhani-Medium.ttf b/Resources/Rajdhani-Medium.ttf new file mode 100644 index 0000000..a6960c6 Binary files /dev/null and b/Resources/Rajdhani-Medium.ttf differ diff --git a/Resources/Rajdhani-Regular.ttf b/Resources/Rajdhani-Regular.ttf new file mode 100644 index 0000000..d25bd37 Binary files /dev/null and b/Resources/Rajdhani-Regular.ttf differ diff --git a/Source/FxPanel.cpp b/Source/FxPanel.cpp index 219152b..6ce7ecc 100644 --- a/Source/FxPanel.cpp +++ b/Source/FxPanel.cpp @@ -26,10 +26,11 @@ void FxPanel::setupKnob (juce::Slider& s, juce::Label& l, const juce::String& na s.setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0); s.setRange (min, max, step); s.setValue (val, juce::dontSendNotification); + s.setDoubleClickReturnValue (true, val); + s.getProperties().set (InstaDrumsLookAndFeel::knobTypeProperty, "dark"); addAndMakeVisible (s); l.setText (name, juce::dontSendNotification); - l.setFont (juce::FontOptions (9.0f)); l.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::textSecondary); l.setJustificationType (juce::Justification::centred); addAndMakeVisible (l); @@ -38,7 +39,6 @@ void FxPanel::setupKnob (juce::Slider& s, juce::Label& l, const juce::String& na void FxPanel::setupTitle (juce::Label& l, const juce::String& text) { l.setText (text, juce::dontSendNotification); - l.setFont (juce::FontOptions (10.0f, juce::Font::bold)); l.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::accent); l.setJustificationType (juce::Justification::centredLeft); addAndMakeVisible (l); @@ -47,70 +47,116 @@ void FxPanel::setupTitle (juce::Label& l, const juce::String& text) void FxPanel::paint (juce::Graphics& g) { auto bounds = getLocalBounds().toFloat(); - g.setColour (InstaDrumsLookAndFeel::bgMedium); + float scale = (float) getHeight() / 220.0f; + + juce::ColourGradient bgGrad (InstaDrumsLookAndFeel::bgMedium, 0, bounds.getY(), + InstaDrumsLookAndFeel::bgMedium.darker (0.2f), 0, bounds.getBottom(), false); + g.setGradientFill (bgGrad); g.fillRoundedRectangle (bounds, 6.0f); - g.setColour (InstaDrumsLookAndFeel::bgLight.withAlpha (0.5f)); + g.setColour (InstaDrumsLookAndFeel::bgLight.withAlpha (0.6f)); g.drawRoundedRectangle (bounds, 6.0f, 1.0f); - // "FX" header + // "FX" header bar + int headerH = (int) (22 * scale); + auto headerBar = bounds.reduced (1).removeFromTop ((float) headerH); + g.setColour (InstaDrumsLookAndFeel::bgDark.withAlpha (0.5f)); + g.fillRoundedRectangle (headerBar.getX(), headerBar.getY(), headerBar.getWidth(), headerBar.getHeight(), 5.0f); g.setColour (InstaDrumsLookAndFeel::textSecondary); - g.setFont (juce::FontOptions (14.0f, juce::Font::bold)); - g.drawText ("FX", bounds.reduced (6, 4).removeFromTop (18), juce::Justification::centredLeft); + g.setFont (juce::FontOptions (std::max (13.0f, 16.0f * scale), juce::Font::bold)); + g.drawText ("FX", headerBar.reduced (8, 0), juce::Justification::centredLeft); + + // Bordered boxes for each FX section + auto area = bounds.reduced (6); + area.removeFromTop ((float) headerH + 4); + float halfW = area.getWidth() / 2; + float rowH = area.getHeight() / 2; + + auto drawFxBox = [&] (juce::Rectangle r) + { + g.setColour (InstaDrumsLookAndFeel::bgDark.withAlpha (0.4f)); + g.fillRoundedRectangle (r, 4.0f); + g.setColour (InstaDrumsLookAndFeel::bgLight.withAlpha (0.35f)); + g.drawRoundedRectangle (r, 4.0f, 1.0f); + }; + + float gap = 3.0f; + drawFxBox ({ area.getX(), area.getY(), halfW - gap, rowH - gap }); + drawFxBox ({ area.getX() + halfW + gap, area.getY(), halfW - gap, rowH - gap }); + drawFxBox ({ area.getX(), area.getY() + rowH + gap, halfW - gap, rowH - gap }); + drawFxBox ({ area.getX() + halfW + gap, area.getY() + rowH + gap, halfW - gap, rowH - gap }); } void FxPanel::resized() { - auto area = getLocalBounds().reduced (6); - area.removeFromTop (20); // FX header + auto area = getLocalBounds().reduced (8); + float scale = (float) getHeight() / 220.0f; + + float titleSize = std::max (10.0f, 13.0f * scale); + float labelSize = std::max (9.0f, 12.0f * scale); + int labelH = (int) (labelSize + 6); + int headerH = (int) (22 * scale); + int titleH = (int) (titleSize + 4); + + area.removeFromTop (headerH + 4); int halfW = area.getWidth() / 2; int rowH = area.getHeight() / 2; - // Top row: Compressor | EQ - auto topRow = area.removeFromTop (rowH); + // Update title fonts + compTitle.setFont (juce::FontOptions (titleSize, juce::Font::bold)); + eqTitle.setFont (juce::FontOptions (titleSize, juce::Font::bold)); + distTitle.setFont (juce::FontOptions (titleSize, juce::Font::bold)); + reverbTitle.setFont (juce::FontOptions (titleSize, juce::Font::bold)); + + auto layoutSection = [&] (juce::Rectangle secArea, juce::Label& title, + juce::Slider* sliders[], juce::Label* labels[], int count) { - auto compArea = topRow.removeFromLeft (halfW).reduced (2); - compTitle.setBounds (compArea.removeFromTop (14)); - int kw = compArea.getWidth() / 2; - auto c1 = compArea.removeFromLeft (kw); - compThreshLabel.setBounds (c1.removeFromBottom (12)); - compThreshSlider.setBounds (c1); - compRatioLabel.setBounds (compArea.removeFromBottom (12)); - compRatioSlider.setBounds (compArea); - } + title.setBounds (secArea.removeFromTop (titleH).reduced (4, 0)); + int kw = secArea.getWidth() / count; + for (int i = 0; i < count; ++i) + { + labels[i]->setFont (juce::FontOptions (labelSize)); + auto col = secArea.removeFromLeft (kw); + labels[i]->setBounds (col.removeFromBottom (labelH)); + sliders[i]->setBounds (col); + } + }; + + // Top-left: Compressor { - auto eqArea = topRow.reduced (2); - eqTitle.setBounds (eqArea.removeFromTop (14)); - int kw = eqArea.getWidth() / 3; - auto c1 = eqArea.removeFromLeft (kw); - eqLoLabel.setBounds (c1.removeFromBottom (12)); - eqLoSlider.setBounds (c1); - auto c2 = eqArea.removeFromLeft (kw); - eqMidLabel.setBounds (c2.removeFromBottom (12)); - eqMidSlider.setBounds (c2); - eqHiLabel.setBounds (eqArea.removeFromBottom (12)); - eqHiSlider.setBounds (eqArea); + auto sec = area.removeFromTop (rowH).removeFromLeft (halfW).reduced (4, 2); + juce::Slider* s[] = { &compThreshSlider, &compRatioSlider }; + juce::Label* l[] = { &compThreshLabel, &compRatioLabel }; + layoutSection (sec, compTitle, s, l, 2); } - // Bottom row: Distortion | Reverb + // Top-right: EQ (need to recalculate since we consumed area) + // Reset area for right side + auto rightTop = getLocalBounds().reduced (8); + rightTop.removeFromTop ((int) (headerH + 4)); + rightTop.removeFromLeft (halfW); + rightTop = rightTop.removeFromTop (rowH).reduced (4, 2); { - auto distArea = area.removeFromLeft (halfW).reduced (2); - distTitle.setBounds (distArea.removeFromTop (14)); - int kw = distArea.getWidth() / 2; - auto c1 = distArea.removeFromLeft (kw); - distDriveLabel.setBounds (c1.removeFromBottom (12)); - distDriveSlider.setBounds (c1); - distMixLabel.setBounds (distArea.removeFromBottom (12)); - distMixSlider.setBounds (distArea); + juce::Slider* s[] = { &eqLoSlider, &eqMidSlider, &eqHiSlider }; + juce::Label* l[] = { &eqLoLabel, &eqMidLabel, &eqHiLabel }; + layoutSection (rightTop, eqTitle, s, l, 3); } + + // Bottom-left: Distortion + auto bottomArea = getLocalBounds().reduced (8); + bottomArea.removeFromTop ((int) (headerH + 4 + rowH)); { - auto revArea = area.reduced (2); - reverbTitle.setBounds (revArea.removeFromTop (14)); - int kw = revArea.getWidth() / 2; - auto c1 = revArea.removeFromLeft (kw); - reverbSizeLabel.setBounds (c1.removeFromBottom (12)); - reverbSizeSlider.setBounds (c1); - reverbDecayLabel.setBounds (revArea.removeFromBottom (12)); - reverbDecaySlider.setBounds (revArea); + auto sec = bottomArea.removeFromLeft (halfW).reduced (4, 2); + juce::Slider* s[] = { &distDriveSlider, &distMixSlider }; + juce::Label* l[] = { &distDriveLabel, &distMixLabel }; + layoutSection (sec, distTitle, s, l, 2); + } + + // Bottom-right: Reverb + { + auto sec = bottomArea.reduced (4, 2); + juce::Slider* s[] = { &reverbSizeSlider, &reverbDecaySlider }; + juce::Label* l[] = { &reverbSizeLabel, &reverbDecayLabel }; + layoutSection (sec, reverbTitle, s, l, 2); } } diff --git a/Source/LookAndFeel.cpp b/Source/LookAndFeel.cpp index 68d6e06..323fffc 100644 --- a/Source/LookAndFeel.cpp +++ b/Source/LookAndFeel.cpp @@ -1,44 +1,271 @@ #include "LookAndFeel.h" +#include "BinaryData.h" InstaDrumsLookAndFeel::InstaDrumsLookAndFeel() { + // Load embedded fonts + typefaceRegular = juce::Typeface::createSystemTypefaceFor ( + BinaryData::RajdhaniRegular_ttf, BinaryData::RajdhaniRegular_ttfSize); + typefaceMedium = juce::Typeface::createSystemTypefaceFor ( + BinaryData::RajdhaniMedium_ttf, BinaryData::RajdhaniMedium_ttfSize); + typefaceBold = juce::Typeface::createSystemTypefaceFor ( + BinaryData::RajdhaniBold_ttf, BinaryData::RajdhaniBold_ttfSize); + setColour (juce::ResizableWindow::backgroundColourId, bgDark); setColour (juce::Label::textColourId, textPrimary); setColour (juce::TextButton::buttonColourId, bgMedium); setColour (juce::TextButton::textColourOffId, textPrimary); + + generateNoiseTexture(); } +juce::Typeface::Ptr InstaDrumsLookAndFeel::getTypefaceForFont (const juce::Font& font) +{ + if (font.isBold()) + return typefaceBold; + return typefaceRegular; +} + +juce::Font InstaDrumsLookAndFeel::getRegularFont (float height) const +{ + return juce::Font (juce::FontOptions (typefaceRegular).withHeight (height)); +} + +juce::Font InstaDrumsLookAndFeel::getMediumFont (float height) const +{ + return juce::Font (juce::FontOptions (typefaceMedium).withHeight (height)); +} + +juce::Font InstaDrumsLookAndFeel::getBoldFont (float height) const +{ + return juce::Font (juce::FontOptions (typefaceBold).withHeight (height)); +} + +// ============================================================ +// Background noise texture (subtle carbon fiber / grain effect) +// ============================================================ + +void InstaDrumsLookAndFeel::generateNoiseTexture() +{ + const int texW = 256, texH = 256; + noiseTexture = juce::Image (juce::Image::ARGB, texW, texH, true); + + juce::Random rng (42); + + for (int y = 0; y < texH; ++y) + { + for (int x = 0; x < texW; ++x) + { + // Subtle noise grain + float noise = rng.nextFloat() * 0.06f; + + // Carbon fiber pattern: diagonal cross-hatch + bool crossA = ((x + y) % 4 == 0); + bool crossB = ((x - y + 256) % 4 == 0); + float pattern = (crossA || crossB) ? 0.03f : 0.0f; + + float alpha = noise + pattern; + noiseTexture.setPixelAt (x, y, juce::Colour::fromFloatRGBA (1.0f, 1.0f, 1.0f, alpha)); + } + } +} + +void InstaDrumsLookAndFeel::drawBackgroundTexture (juce::Graphics& g, juce::Rectangle area) +{ + // Tile the noise texture + for (int y = area.getY(); y < area.getBottom(); y += noiseTexture.getHeight()) + for (int x = area.getX(); x < area.getRight(); x += noiseTexture.getWidth()) + g.drawImageAt (noiseTexture, x, y); +} + +// ============================================================ +// Rotary slider (3D metal knob) +// ============================================================ + void InstaDrumsLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height, float sliderPos, float rotaryStartAngle, float rotaryEndAngle, juce::Slider& slider) { - auto bounds = juce::Rectangle (x, y, width, height).toFloat().reduced (2.0f); + // Scale factor based on knob pixel size (reference: 60px) + float knobSize = std::min ((float) width, (float) height); + float s = knobSize / 60.0f; // 1.0 at 60px, smaller below, larger above + float margin = std::max (4.0f, 6.0f * s); + + auto bounds = juce::Rectangle (x, y, width, height).toFloat().reduced (margin); auto radius = std::min (bounds.getWidth(), bounds.getHeight()) / 2.0f; - auto centreX = bounds.getCentreX(); - auto centreY = bounds.getCentreY(); + auto cx = bounds.getCentreX(); + auto cy = bounds.getCentreY(); auto angle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle); - // Background arc - juce::Path bgArc; - bgArc.addCentredArc (centreX, centreY, radius - 2, radius - 2, - 0.0f, rotaryStartAngle, rotaryEndAngle, true); - g.setColour (bgLight); - g.strokePath (bgArc, juce::PathStrokeType (3.0f, juce::PathStrokeType::curved, - juce::PathStrokeType::rounded)); + auto knobType = slider.getProperties() [knobTypeProperty].toString(); + bool isDark = (knobType == "dark"); - // Value arc - juce::Path valueArc; - valueArc.addCentredArc (centreX, centreY, radius - 2, radius - 2, - 0.0f, rotaryStartAngle, angle, true); - g.setColour (accent); - g.strokePath (valueArc, juce::PathStrokeType (3.0f, juce::PathStrokeType::curved, - juce::PathStrokeType::rounded)); + juce::Colour arcColour = isDark ? juce::Colour (0xff4488ff) : juce::Colour (0xffff8833); + juce::Colour arcBgColour = isDark ? juce::Colour (0xff1a2a44) : juce::Colour (0xff2a1a0a); + juce::Colour bodyTop = isDark ? juce::Colour (0xff3a3a4a) : juce::Colour (0xff5a4a3a); + juce::Colour bodyBottom = isDark ? juce::Colour (0xff1a1a2a) : juce::Colour (0xff2a1a0a); + juce::Colour rimColour = isDark ? juce::Colour (0xff555566) : juce::Colour (0xff886644); + juce::Colour highlightCol = isDark ? juce::Colour (0x33aabbff) : juce::Colour (0x44ffcc88); + juce::Colour pointerColour = isDark ? juce::Colour (0xff66aaff) : juce::Colour (0xffffaa44); - // Pointer - juce::Path pointer; - auto pointerLength = radius * 0.5f; - pointer.addRectangle (-1.5f, -pointerLength, 3.0f, pointerLength); - pointer.applyTransform (juce::AffineTransform::rotation (angle).translated (centreX, centreY)); - g.setColour (textPrimary); - g.fillPath (pointer); + // All thicknesses scale with knob size + float arcW = std::max (1.5f, 2.5f * s); // core arc width + float glowW1 = std::max (3.0f, 10.0f * s); // outermost glow + float glowW2 = std::max (2.5f, 7.0f * s); // mid glow + float glowW3 = std::max (2.0f, 4.5f * s); // inner glow + float hotW = std::max (0.8f, 1.2f * s); // hot center + float ptrW = std::max (1.2f, 2.0f * s); // pointer width + float bodyRadius = radius * 0.72f; + + // 1. Drop shadow + g.setColour (juce::Colours::black.withAlpha (0.35f)); + g.fillEllipse (cx - bodyRadius + 1, cy - bodyRadius + 2, bodyRadius * 2, bodyRadius * 2); + + // 2. Outer arc track + { + juce::Path arcBg; + arcBg.addCentredArc (cx, cy, radius - 1, radius - 1, 0.0f, + rotaryStartAngle, rotaryEndAngle, true); + g.setColour (arcBgColour); + g.strokePath (arcBg, juce::PathStrokeType (arcW, juce::PathStrokeType::curved, + juce::PathStrokeType::rounded)); + } + + // 3. Outer arc value with scaled glow + if (sliderPos > 0.01f) + { + juce::Path arcVal; + arcVal.addCentredArc (cx, cy, radius - 1, radius - 1, 0.0f, + rotaryStartAngle, angle, true); + + // Glow layers (scale with knob size) + g.setColour (arcColour.withAlpha (0.08f)); + g.strokePath (arcVal, juce::PathStrokeType (glowW1, juce::PathStrokeType::curved, + juce::PathStrokeType::rounded)); + g.setColour (arcColour.withAlpha (0.15f)); + g.strokePath (arcVal, juce::PathStrokeType (glowW2, juce::PathStrokeType::curved, + juce::PathStrokeType::rounded)); + g.setColour (arcColour.withAlpha (0.3f)); + g.strokePath (arcVal, juce::PathStrokeType (glowW3, juce::PathStrokeType::curved, + juce::PathStrokeType::rounded)); + + // Core arc + g.setColour (arcColour); + g.strokePath (arcVal, juce::PathStrokeType (arcW, juce::PathStrokeType::curved, + juce::PathStrokeType::rounded)); + + // Hot center + g.setColour (arcColour.brighter (0.6f).withAlpha (0.5f)); + g.strokePath (arcVal, juce::PathStrokeType (hotW, juce::PathStrokeType::curved, + juce::PathStrokeType::rounded)); + } + + // 4. Knob body — radial gradient + { + juce::ColourGradient bodyGrad (bodyTop, cx, cy - bodyRadius * 0.5f, + bodyBottom, cx, cy + bodyRadius, true); + g.setGradientFill (bodyGrad); + g.fillEllipse (cx - bodyRadius, cy - bodyRadius, bodyRadius * 2, bodyRadius * 2); + } + + // 5. Rim + g.setColour (rimColour.withAlpha (0.6f)); + g.drawEllipse (cx - bodyRadius, cy - bodyRadius, bodyRadius * 2, bodyRadius * 2, std::max (0.8f, 1.2f * s)); + + // 6. Inner shadow + { + float innerR = bodyRadius * 0.85f; + juce::ColourGradient innerGrad (juce::Colours::black.withAlpha (0.15f), cx, cy - innerR * 0.3f, + juce::Colours::transparentBlack, cx, cy + innerR, true); + g.setGradientFill (innerGrad); + g.fillEllipse (cx - innerR, cy - innerR, innerR * 2, innerR * 2); + } + + // 7. Top highlight + { + float hlRadius = bodyRadius * 0.55f; + float hlY = cy - bodyRadius * 0.35f; + juce::ColourGradient hlGrad (highlightCol, cx, hlY - hlRadius * 0.5f, + juce::Colours::transparentBlack, cx, hlY + hlRadius, true); + g.setGradientFill (hlGrad); + g.fillEllipse (cx - hlRadius, hlY - hlRadius * 0.6f, hlRadius * 2, hlRadius * 1.2f); + } + + // 8. Pointer with scaled glow + { + juce::Path pointer; + float pointerLen = bodyRadius * 0.75f; + + pointer.addRoundedRectangle (-ptrW * 0.5f, -pointerLen, ptrW, pointerLen * 0.55f, ptrW * 0.5f); + pointer.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy)); + + // Wide outer glow + g.setColour (pointerColour.withAlpha (0.1f)); + { + juce::Path glow3; + float gw = ptrW * 3.5f; + glow3.addRoundedRectangle (-gw, -pointerLen, gw * 2, pointerLen * 0.55f, ptrW * 1.5f); + glow3.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy)); + g.fillPath (glow3); + } + + // Medium glow + g.setColour (pointerColour.withAlpha (0.25f)); + { + juce::Path glow2; + float gw = ptrW * 2.0f; + glow2.addRoundedRectangle (-gw, -pointerLen, gw * 2, pointerLen * 0.55f, ptrW); + glow2.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy)); + g.fillPath (glow2); + } + + // Core pointer + g.setColour (pointerColour); + g.fillPath (pointer); + + // Hot center + { + juce::Path hotCenter; + float hw = ptrW * 0.3f; + hotCenter.addRoundedRectangle (-hw, -pointerLen, hw * 2, pointerLen * 0.5f, hw); + hotCenter.applyTransform (juce::AffineTransform::rotation (angle).translated (cx, cy)); + g.setColour (pointerColour.brighter (0.7f).withAlpha (0.6f)); + g.fillPath (hotCenter); + } + } + + // 9. Center cap + { + float capR = bodyRadius * 0.18f; + juce::ColourGradient capGrad (rimColour.brighter (0.3f), cx, cy - capR, + bodyBottom, cx, cy + capR, false); + g.setGradientFill (capGrad); + g.fillEllipse (cx - capR, cy - capR, capR * 2, capR * 2); + } +} + +// ============================================================ +// Button style +// ============================================================ + +void InstaDrumsLookAndFeel::drawButtonBackground (juce::Graphics& g, juce::Button& button, + const juce::Colour& backgroundColour, + bool shouldDrawButtonAsHighlighted, + bool shouldDrawButtonAsDown) +{ + auto bounds = button.getLocalBounds().toFloat().reduced (0.5f); + + auto baseColour = backgroundColour; + if (shouldDrawButtonAsDown) + baseColour = baseColour.brighter (0.2f); + else if (shouldDrawButtonAsHighlighted) + baseColour = baseColour.brighter (0.1f); + + juce::ColourGradient grad (baseColour.brighter (0.05f), 0, bounds.getY(), + baseColour.darker (0.1f), 0, bounds.getBottom(), false); + g.setGradientFill (grad); + g.fillRoundedRectangle (bounds, 4.0f); + + g.setColour (bgLight.withAlpha (shouldDrawButtonAsHighlighted ? 0.8f : 0.5f)); + g.drawRoundedRectangle (bounds, 4.0f, 1.0f); } diff --git a/Source/LookAndFeel.h b/Source/LookAndFeel.h index d184c5a..ea2eeb8 100644 --- a/Source/LookAndFeel.h +++ b/Source/LookAndFeel.h @@ -12,9 +12,35 @@ public: static inline const juce::Colour textSecondary { 0xff888899 }; static inline const juce::Colour accent { 0xff00ff88 }; + // Knob type property key + static constexpr const char* knobTypeProperty = "knobType"; + InstaDrumsLookAndFeel(); void drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height, float sliderPosProportional, float rotaryStartAngle, float rotaryEndAngle, juce::Slider& slider) override; + + void drawButtonBackground (juce::Graphics& g, juce::Button& button, + const juce::Colour& backgroundColour, + bool shouldDrawButtonAsHighlighted, + bool shouldDrawButtonAsDown) override; + + // Custom fonts + juce::Font getRegularFont (float height) const; + juce::Font getMediumFont (float height) const; + juce::Font getBoldFont (float height) const; + + // Background texture + void drawBackgroundTexture (juce::Graphics& g, juce::Rectangle area); + + juce::Typeface::Ptr getTypefaceForFont (const juce::Font& font) override; + +private: + juce::Typeface::Ptr typefaceRegular; + juce::Typeface::Ptr typefaceMedium; + juce::Typeface::Ptr typefaceBold; + + juce::Image noiseTexture; + void generateNoiseTexture(); }; diff --git a/Source/MasterPanel.cpp b/Source/MasterPanel.cpp index f06d16c..26b7de3 100644 --- a/Source/MasterPanel.cpp +++ b/Source/MasterPanel.cpp @@ -3,7 +3,6 @@ MasterPanel::MasterPanel() { - masterTitle.setFont (juce::FontOptions (12.0f, juce::Font::bold)); masterTitle.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::textSecondary); addAndMakeVisible (masterTitle); @@ -21,10 +20,10 @@ void MasterPanel::setupKnob (juce::Slider& s, juce::Label& l, const juce::String s.setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0); s.setRange (min, max, step); s.setValue (val, juce::dontSendNotification); + s.setDoubleClickReturnValue (true, val); addAndMakeVisible (s); l.setText (name, juce::dontSendNotification); - l.setFont (juce::FontOptions (9.0f)); l.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::textSecondary); l.setJustificationType (juce::Justification::centred); addAndMakeVisible (l); @@ -41,22 +40,26 @@ void MasterPanel::paint (juce::Graphics& g) void MasterPanel::resized() { - auto area = getLocalBounds().reduced (4); + auto area = getLocalBounds().reduced (6); + float scale = (float) getHeight() / 52.0f; + float titleSize = std::max (12.0f, 16.0f * scale); + float labelSize = std::max (9.0f, 12.0f * scale); + int labelH = (int) (labelSize + 4); - masterTitle.setBounds (area.removeFromLeft (55).reduced (0, 2)); + masterTitle.setFont (juce::FontOptions (titleSize, juce::Font::bold)); + masterTitle.setBounds (area.removeFromLeft ((int) (65 * scale)).reduced (0, 2)); - // VU meter on the right - vuMeter.setBounds (area.removeFromRight (24).reduced (0, 2)); + vuMeter.setBounds (area.removeFromRight ((int) (28 * scale)).reduced (0, 2)); area.removeFromRight (4); - // Knobs int knobW = area.getWidth() / 3; juce::Slider* sliders[] = { &volumeSlider, &tuneSlider, &panSlider }; juce::Label* labels[] = { &volumeLabel, &tuneLabel, &panLabel }; for (int i = 0; i < 3; ++i) { + labels[i]->setFont (juce::FontOptions (labelSize)); auto col = area.removeFromLeft (knobW); - labels[i]->setBounds (col.removeFromBottom (12)); + labels[i]->setBounds (col.removeFromBottom (labelH)); sliders[i]->setBounds (col); } } diff --git a/Source/PadComponent.cpp b/Source/PadComponent.cpp index cca6742..8184c44 100644 --- a/Source/PadComponent.cpp +++ b/Source/PadComponent.cpp @@ -11,39 +11,66 @@ void PadComponent::paint (juce::Graphics& g) auto bounds = getLocalBounds().toFloat().reduced (2.0f); float cornerSize = 6.0f; - // Background - float alpha = isPressed ? 0.85f : (isDragOver ? 0.65f : (selected ? 0.55f : 0.3f)); - g.setColour (drumPad.colour.withAlpha (alpha)); + // Background — darker, less saturated base + float alpha = isPressed ? 0.6f : (isDragOver ? 0.45f : (selected ? 0.35f : 0.18f)); + g.setColour (drumPad.colour.withSaturation (0.5f).withAlpha (alpha)); g.fillRoundedRectangle (bounds, cornerSize); - // Border — selected = bright accent, normal = pad colour + // Inner gradient for depth + { + juce::ColourGradient innerGrad (juce::Colours::white.withAlpha (0.04f), bounds.getCentreX(), bounds.getY(), + juce::Colours::black.withAlpha (0.08f), bounds.getCentreX(), bounds.getBottom(), false); + g.setGradientFill (innerGrad); + g.fillRoundedRectangle (bounds, cornerSize); + } + + // Border — selected = cyan glow, normal = subtle if (selected) { - g.setColour (juce::Colour (0xff00aaff)); // blue selection - g.drawRoundedRectangle (bounds, cornerSize, 2.5f); + // Outer glow effect (multiple passes) + auto glowColour = juce::Colour (0xff00aaff); + g.setColour (glowColour.withAlpha (0.1f)); + g.drawRoundedRectangle (bounds.expanded (2), cornerSize + 1, 4.0f); + g.setColour (glowColour.withAlpha (0.25f)); + g.drawRoundedRectangle (bounds.expanded (0.5f), cornerSize, 2.0f); + g.setColour (glowColour); + g.drawRoundedRectangle (bounds, cornerSize, 1.5f); } else { - g.setColour (drumPad.colour.withAlpha (0.6f)); + g.setColour (drumPad.colour.withAlpha (0.4f)); g.drawRoundedRectangle (bounds, cornerSize, 1.0f); } - // Pad number (top-left, small) - g.setColour (InstaDrumsLookAndFeel::textSecondary); - g.setFont (juce::Font (juce::FontOptions (9.0f, juce::Font::bold))); - g.drawText (juce::String (index + 1), bounds.reduced (4, 3), juce::Justification::topLeft); + // Scale fonts to pad size + float padScale = std::min (bounds.getWidth(), bounds.getHeight()) / 100.0f; + float numFontSize = std::max (9.0f, 13.0f * padScale); + float nameFontSize = std::max (9.0f, 14.0f * padScale); + float numBoxH = numFontSize + 4; + float numBoxW = numBoxH + 4; + + // Pad number (top-left, with small dark background for contrast) + { + auto numBounds = bounds.reduced (5, 4).removeFromTop (numBoxH).removeFromLeft (numBoxW); + g.setColour (juce::Colours::black.withAlpha (0.3f)); + g.fillRoundedRectangle (numBounds, 3.0f); + g.setColour (drumPad.colour.brighter (0.4f)); + g.setFont (juce::Font (juce::FontOptions (numFontSize, juce::Font::bold))); + g.drawText (juce::String (index + 1), numBounds, juce::Justification::centred); + } // Waveform thumbnail (center area) if (drumPad.hasSample()) { - auto waveArea = bounds.reduced (4, 16); + auto waveArea = bounds.reduced (5, numBoxH + 6); + waveArea.removeFromBottom (nameFontSize + 4); drawWaveformThumbnail (g, waveArea); } // Pad name (bottom) g.setColour (InstaDrumsLookAndFeel::textPrimary); - g.setFont (juce::Font (juce::FontOptions (10.0f, juce::Font::bold))); - g.drawText (drumPad.name, bounds.reduced (4, 2), juce::Justification::centredBottom); + g.setFont (juce::Font (juce::FontOptions (nameFontSize, juce::Font::bold))); + g.drawText (drumPad.name, bounds.reduced (5, 3), juce::Justification::centredBottom); // Playing flash if (drumPad.isPlaying()) diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index c73e39a..6013b33 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -4,19 +4,20 @@ InstaDrumsEditor::InstaDrumsEditor (InstaDrumsProcessor& p) : AudioProcessorEditor (&p), processor (p) { setLookAndFeel (&customLookAndFeel); + juce::LookAndFeel::setDefaultLookAndFeel (&customLookAndFeel); // Title - titleLabel.setFont (juce::FontOptions (20.0f, juce::Font::bold)); + titleLabel.setFont (customLookAndFeel.getBoldFont (25.0f)); titleLabel.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::accent); titleLabel.setJustificationType (juce::Justification::centredLeft); addAndMakeVisible (titleLabel); - versionLabel.setFont (juce::FontOptions (10.0f)); + versionLabel.setFont (juce::FontOptions (12.5f)); versionLabel.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::textSecondary); versionLabel.setJustificationType (juce::Justification::centredRight); addAndMakeVisible (versionLabel); - padsLabel.setFont (juce::FontOptions (10.0f, juce::Font::bold)); + padsLabel.setFont (juce::FontOptions (12.5f, juce::Font::bold)); padsLabel.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::textSecondary); addAndMakeVisible (padsLabel); @@ -108,6 +109,7 @@ InstaDrumsEditor::InstaDrumsEditor (InstaDrumsProcessor& p) InstaDrumsEditor::~InstaDrumsEditor() { + juce::LookAndFeel::setDefaultLookAndFeel (nullptr); setLookAndFeel (nullptr); } @@ -145,14 +147,30 @@ void InstaDrumsEditor::selectPad (int index) void InstaDrumsEditor::paint (juce::Graphics& g) { - g.fillAll (InstaDrumsLookAndFeel::bgDark); + // Background gradient + juce::ColourGradient bgGrad (InstaDrumsLookAndFeel::bgDark, 0, 0, + InstaDrumsLookAndFeel::bgDark.darker (0.3f), 0, (float) getHeight(), false); + g.setGradientFill (bgGrad); + g.fillAll(); - // Subtle divider lines + // Noise texture overlay + customLookAndFeel.drawBackgroundTexture (g, getLocalBounds()); + + // Top header bar + float sc = (float) getHeight() / 600.0f; + int topH = std::max (28, (int) (36 * sc)); + g.setColour (InstaDrumsLookAndFeel::bgDark.darker (0.4f)); + g.fillRect (0, 0, getWidth(), topH); + g.setColour (InstaDrumsLookAndFeel::bgLight.withAlpha (0.3f)); + g.drawHorizontalLine (topH, 0, (float) getWidth()); + + // Divider lines auto bounds = getLocalBounds(); int rightPanelX = (int) (bounds.getWidth() * 0.52f); - int bottomPanelY = bounds.getHeight() - 56; + int masterH = std::max (44, (int) (60 * sc)); + int bottomPanelY = bounds.getHeight() - masterH - 4; - g.setColour (InstaDrumsLookAndFeel::bgLight.withAlpha (0.3f)); + g.setColour (InstaDrumsLookAndFeel::bgLight.withAlpha (0.4f)); g.drawVerticalLine (rightPanelX - 2, 30, (float) bottomPanelY); g.drawHorizontalLine (bottomPanelY - 1, 0, (float) bounds.getWidth()); } @@ -160,25 +178,38 @@ void InstaDrumsEditor::paint (juce::Graphics& g) void InstaDrumsEditor::resized() { auto area = getLocalBounds(); + float scale = (float) getHeight() / 600.0f; - // Top bar (30px) - auto topBar = area.removeFromTop (30).reduced (6, 4); - titleLabel.setBounds (topBar.removeFromLeft (150)); - versionLabel.setBounds (topBar.removeFromRight (40)); - loadFolderButton.setBounds (topBar.removeFromRight (90).reduced (1)); - loadKitButton.setBounds (topBar.removeFromRight (70).reduced (1)); - saveKitButton.setBounds (topBar.removeFromRight (70).reduced (1)); - loadSampleButton.setBounds (topBar.removeFromRight (95).reduced (1)); + // Top bar + int topBarH = std::max (28, (int) (36 * scale)); + auto topBar = area.removeFromTop (topBarH).reduced (8, 4); - // Bottom master bar (52px) - masterPanel.setBounds (area.removeFromBottom (52).reduced (4, 2)); + float titleSize = std::max (18.0f, 26.0f * scale); + titleLabel.setFont (customLookAndFeel.getBoldFont (titleSize)); + titleLabel.setBounds (topBar.removeFromLeft ((int) (180 * scale))); + + float smallSize = std::max (10.0f, 13.0f * scale); + versionLabel.setFont (juce::FontOptions (smallSize)); + versionLabel.setBounds (topBar.removeFromRight ((int) (50 * scale))); + + int btnW = std::max (60, (int) (90 * scale)); + loadFolderButton.setBounds (topBar.removeFromRight (btnW).reduced (2)); + loadKitButton.setBounds (topBar.removeFromRight ((int) (btnW * 0.8f)).reduced (2)); + saveKitButton.setBounds (topBar.removeFromRight ((int) (btnW * 0.8f)).reduced (2)); + loadSampleButton.setBounds (topBar.removeFromRight (btnW).reduced (2)); + + // Bottom master bar + int masterH = std::max (44, (int) (60 * scale)); + masterPanel.setBounds (area.removeFromBottom (masterH).reduced (4, 2)); // Left panel: pad grid (~52% width) int rightPanelX = (int) (area.getWidth() * 0.52f); - auto leftArea = area.removeFromLeft (rightPanelX).reduced (4); + auto leftArea = area.removeFromLeft (rightPanelX).reduced (6); // Pads label - auto padsHeader = leftArea.removeFromTop (16); + int padsLabelH = (int) (20 * scale); + padsLabel.setFont (juce::FontOptions (std::max (11.0f, 14.0f * scale), juce::Font::bold)); + auto padsHeader = leftArea.removeFromTop (padsLabelH); padsLabel.setBounds (padsHeader); // Pad grid diff --git a/Source/SampleEditorPanel.cpp b/Source/SampleEditorPanel.cpp index 8149c4c..2ad3786 100644 --- a/Source/SampleEditorPanel.cpp +++ b/Source/SampleEditorPanel.cpp @@ -3,11 +3,9 @@ SampleEditorPanel::SampleEditorPanel() { - titleLabel.setFont (juce::FontOptions (14.0f, juce::Font::bold)); titleLabel.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::textSecondary); addAndMakeVisible (titleLabel); - padNameLabel.setFont (juce::FontOptions (13.0f, juce::Font::bold)); padNameLabel.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::accent); addAndMakeVisible (padNameLabel); @@ -24,6 +22,9 @@ SampleEditorPanel::SampleEditorPanel() setupKnob (resoSlider, resoLabel, "Reso", 0.1, 10.0, 0.707, 0.01); cutoffSlider.setSkewFactorFromMidPoint (1000.0); + + cutoffSlider.getProperties().set (InstaDrumsLookAndFeel::knobTypeProperty, "dark"); + resoSlider.getProperties().set (InstaDrumsLookAndFeel::knobTypeProperty, "dark"); } void SampleEditorPanel::setupKnob (juce::Slider& s, juce::Label& l, const juce::String& name, @@ -33,11 +34,11 @@ void SampleEditorPanel::setupKnob (juce::Slider& s, juce::Label& l, const juce:: s.setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0); s.setRange (min, max, step); s.setValue (val, juce::dontSendNotification); + s.setDoubleClickReturnValue (true, val); s.addListener (this); addAndMakeVisible (s); l.setText (name, juce::dontSendNotification); - l.setFont (juce::FontOptions (9.0f)); l.setColour (juce::Label::textColourId, InstaDrumsLookAndFeel::textSecondary); l.setJustificationType (juce::Justification::centred); addAndMakeVisible (l); @@ -85,7 +86,6 @@ void SampleEditorPanel::sliderValueChanged (juce::Slider* slider) else if (slider == &cutoffSlider) currentPad->filterCutoff = (float) slider->getValue(); else if (slider == &resoSlider) currentPad->filterReso = (float) slider->getValue(); - // Update ADSR overlay waveform.setADSR (currentPad->attack, currentPad->decay, currentPad->sustain, currentPad->release); } @@ -100,37 +100,51 @@ void SampleEditorPanel::paint (juce::Graphics& g) void SampleEditorPanel::resized() { - auto area = getLocalBounds().reduced (6); + auto area = getLocalBounds().reduced (8); + + // Scale factor based on component height (reference: 280px) + float scale = (float) getHeight() / 280.0f; + float titleSize = std::max (13.0f, 18.0f * scale); + float labelSize = std::max (10.0f, 13.0f * scale); + int labelH = (int) (labelSize + 6); + int headerH = (int) (titleSize + 8); + int spacing = std::max (4, (int) (8 * scale)); // Header - auto header = area.removeFromTop (20); - titleLabel.setBounds (header.removeFromLeft (100)); + titleLabel.setFont (juce::FontOptions (titleSize, juce::Font::bold)); + padNameLabel.setFont (juce::FontOptions (titleSize, juce::Font::bold)); + + auto header = area.removeFromTop (headerH); + titleLabel.setBounds (header.removeFromLeft ((int) (120 * scale))); padNameLabel.setBounds (header); - area.removeFromTop (2); + area.removeFromTop (spacing); - // Waveform (top portion ~40%) - int waveHeight = std::max (60, (int) (area.getHeight() * 0.38f)); + // Waveform + int waveHeight = std::max (50, (int) (area.getHeight() * 0.36f)); waveform.setBounds (area.removeFromTop (waveHeight)); - area.removeFromTop (4); + area.removeFromTop (spacing); // ADSR knobs row - int knobH = std::max (40, (int) (area.getHeight() * 0.45f)); - auto adsrRow = area.removeFromTop (knobH); + int knobH = (area.getHeight() - spacing - labelH * 2) / 2; + knobH = std::max (30, knobH); + + auto adsrRow = area.removeFromTop (knobH + labelH); int knobW = adsrRow.getWidth() / 4; { juce::Slider* s[] = { &attackSlider, &decaySlider, &sustainSlider, &releaseSlider }; juce::Label* l[] = { &attackLabel, &decayLabel, &sustainLabel, &releaseLabel }; for (int i = 0; i < 4; ++i) { + l[i]->setFont (juce::FontOptions (labelSize)); auto col = adsrRow.removeFromLeft (knobW); - l[i]->setBounds (col.removeFromBottom (14)); + l[i]->setBounds (col.removeFromBottom (labelH)); s[i]->setBounds (col); } } - area.removeFromTop (2); + area.removeFromTop (spacing); // Bottom row: Pitch, Pan, Cutoff, Reso auto bottomRow = area; @@ -140,8 +154,9 @@ void SampleEditorPanel::resized() juce::Label* l[] = { &pitchLabel, &panLabel, &cutoffLabel, &resoLabel }; for (int i = 0; i < 4; ++i) { + l[i]->setFont (juce::FontOptions (labelSize)); auto col = bottomRow.removeFromLeft (knobW); - l[i]->setBounds (col.removeFromBottom (14)); + l[i]->setBounds (col.removeFromBottom (labelH)); s[i]->setBounds (col); } } diff --git a/Source/VuMeter.h b/Source/VuMeter.h index 4f9e83f..ac18053 100644 --- a/Source/VuMeter.h +++ b/Source/VuMeter.h @@ -6,6 +6,12 @@ class VuMeter : public juce::Component public: void setLevel (float left, float right) { + // Update peak hold + if (left > peakL) peakL = left; + else peakL *= 0.995f; + if (right > peakR) peakR = right; + else peakR *= 0.995f; + levelL = left; levelR = right; repaint(); @@ -14,34 +20,60 @@ public: void paint (juce::Graphics& g) override { auto bounds = getLocalBounds().toFloat().reduced (1); - float halfW = bounds.getWidth() / 2.0f - 1; + float barGap = 2.0f; + float halfW = (bounds.getWidth() - barGap) / 2.0f; auto leftBar = bounds.removeFromLeft (halfW); - bounds.removeFromLeft (2); + bounds.removeFromLeft (barGap); auto rightBar = bounds; - drawBar (g, leftBar, levelL); - drawBar (g, rightBar, levelR); + drawBar (g, leftBar, levelL, peakL); + drawBar (g, rightBar, levelR, peakR); + + // Scale markers + g.setColour (juce::Colour (0x44ffffff)); + g.setFont (juce::FontOptions (9.0f)); + float totalH = getLocalBounds().toFloat().getHeight(); + // dB markers: 0dB = top, -6, -12, -24, -48 + float dbLevels[] = { 1.0f, 0.5f, 0.25f, 0.063f, 0.004f }; + const char* dbLabels[] = { "0", "-6", "-12", "-24", "-48" }; + for (int i = 0; i < 5; ++i) + { + float yPos = (1.0f - dbLevels[i]) * totalH; + g.drawHorizontalLine ((int) yPos, 0.0f, (float) getWidth()); + } } private: float levelL = 0.0f, levelR = 0.0f; + float peakL = 0.0f, peakR = 0.0f; - void drawBar (juce::Graphics& g, juce::Rectangle bar, float level) + void drawBar (juce::Graphics& g, juce::Rectangle bar, float level, float peak) { - g.setColour (juce::Colour (0xff222233)); + // Background + g.setColour (juce::Colour (0xff111122)); g.fillRoundedRectangle (bar, 2.0f); - float h = bar.getHeight() * juce::jlimit (0.0f, 1.0f, level); - auto filled = bar.removeFromBottom (h); + float clampedLevel = juce::jlimit (0.0f, 1.0f, level); + float h = bar.getHeight() * clampedLevel; + auto filled = bar.withTop (bar.getBottom() - h); - // Green -> Yellow -> Red gradient - if (level < 0.6f) + // Segmented colour + if (clampedLevel < 0.6f) g.setColour (juce::Colour (0xff00cc44)); - else if (level < 0.85f) + else if (clampedLevel < 0.85f) g.setColour (juce::Colour (0xffcccc00)); else g.setColour (juce::Colour (0xffff3333)); g.fillRoundedRectangle (filled, 2.0f); + + // Peak hold line + float clampedPeak = juce::jlimit (0.0f, 1.0f, peak); + if (clampedPeak > 0.01f) + { + float peakY = bar.getBottom() - bar.getHeight() * clampedPeak; + g.setColour (juce::Colours::white.withAlpha (0.8f)); + g.fillRect (bar.getX(), peakY, bar.getWidth(), 1.5f); + } } }; diff --git a/Source/WaveformDisplay.cpp b/Source/WaveformDisplay.cpp index d525ef5..9fafd5a 100644 --- a/Source/WaveformDisplay.cpp +++ b/Source/WaveformDisplay.cpp @@ -14,9 +14,30 @@ void WaveformDisplay::paint (juce::Graphics& g) { auto bounds = getLocalBounds().toFloat(); - // Background - g.setColour (InstaDrumsLookAndFeel::bgDark.darker (0.3f)); - g.fillRoundedRectangle (bounds, 4.0f); + // Background with subtle gradient + { + juce::ColourGradient bgGrad (InstaDrumsLookAndFeel::bgDark.darker (0.4f), 0, bounds.getY(), + InstaDrumsLookAndFeel::bgDark.darker (0.2f), 0, bounds.getBottom(), false); + g.setGradientFill (bgGrad); + g.fillRoundedRectangle (bounds, 4.0f); + } + + // Border + g.setColour (InstaDrumsLookAndFeel::bgLight.withAlpha (0.3f)); + g.drawRoundedRectangle (bounds, 4.0f, 1.0f); + + // Grid lines (vertical) + g.setColour (InstaDrumsLookAndFeel::bgLight.withAlpha (0.12f)); + for (int i = 1; i < 8; ++i) + { + float xLine = bounds.getX() + bounds.getWidth() * (float) i / 8.0f; + g.drawVerticalLine ((int) xLine, bounds.getY(), bounds.getBottom()); + } + + // Center line (horizontal) + g.setColour (InstaDrumsLookAndFeel::bgLight.withAlpha (0.2f)); + float midY = bounds.getCentreY(); + g.drawHorizontalLine ((int) midY, bounds.getX(), bounds.getRight()); if (audioBuffer == nullptr || audioBuffer->getNumSamples() == 0) return; @@ -27,7 +48,7 @@ void WaveformDisplay::paint (juce::Graphics& g) const int visibleSamples = std::max (1, endSample - startSample); const float width = bounds.getWidth(); const float height = bounds.getHeight(); - const float midY = bounds.getCentreY(); + // midY already declared above // Draw waveform juce::Path wavePath;