Add thumbnails in timeline (#340)

Reviewed-by: David Capello <davidcapello@gmail.com>
This commit is contained in:
Carlo 'zED' Caputo 2016-07-25 09:51:58 -03:00 committed by David Capello
parent 7849b8804d
commit 57567af06b
14 changed files with 453 additions and 3 deletions

View File

@ -106,6 +106,7 @@
<key command="SnapToGrid" shortcut="Shift+S" />
<key command="SetLoopSection" shortcut="F2" />
<key command="ShowOnionSkin" shortcut="F3" />
<key command="ToggleTimelineThumbnails" shortcut="F6" />
<key command="Timeline" shortcut="Tab">
<param name="switch" value="true" />
</key>

View File

@ -197,6 +197,9 @@
<section id="symmetry_mode">
<option id="enabled" type="bool" default="false" />
</section>
<section id="thumbnails">
<option id="cache_limit" type="int" default="5000" />
</section>
</global>
<tool>
@ -253,6 +256,14 @@
<option id="color1" type="app::Color" default="app::Color::fromRgb(128, 128, 128)" migrate="Option.CheckedBgColor1" />
<option id="color2" type="app::Color" default="app::Color::fromRgb(192, 192, 192)" migrate="Option.CheckedBgColor2" />
</section>
<section id="thumbnails">
<option id="enabled" type="bool" default="false" />
<option id="overlay_enabled" type="bool" default="false" />
<option id="overlay_size" type="int" default="5" />
<option id="quality" type="doc::algorithm::ResizeMethod" default="doc::algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR" />
<option id="opacity" type="int" default="255" />
<option id="background" type="app::Color" default="app::Color::fromRgb(180, 180, 180, 220)" />
</section>
<section id="onionskin">
<option id="active" type="bool" default="false" migrate="Onionskin.Enabled" />
<option id="prev_frames" type="int" default="1" migrate="Onionskin.PrevFrames" />

View File

@ -2,6 +2,23 @@
<!-- Copyright (C) 2014, 2015 by David Capello -->
<gui>
<vbox id="timeline_conf">
<separator cell_hspan="2" text="Thumbnails:" left="true" horizontal="true" />
<grid columns="3">
<check id="thumb_enabled" text="Enabled" />
<label text="Opacity:" />
<slider min="0" max="255" id="thumb_opacity" cell_align="horizontal" width="128" />
<check id="thumb_overlay_enabled" text="Overlay"/>
<label text="Size:" />
<slider min="2" max="10" id="thumb_overlay_size" cell_align="horizontal" width="128" />
<label text="Quality:" />
<combobox id="thumb_quality" expansive="true" cell_hspan="2" />
<label text="Background:" />
<colorpicker id="thumb_background" expansive="true" cell_hspan="2" />
</grid>
<separator cell_hspan="2" text="Onion Skin:" left="true" horizontal="true" />
<grid columns="2">
<hbox cell_hspan="2">

View File

@ -228,6 +228,7 @@ add_library(app-lib
commands/cmd_new_layer_set.cpp
commands/cmd_new_sprite_from_selection.cpp
commands/cmd_onionskin.cpp
commands/cmd_toggle_timeline_thumbnails.cpp
commands/cmd_open_file.cpp
commands/cmd_open_in_folder.cpp
commands/cmd_open_with_app.cpp
@ -426,6 +427,7 @@ add_library(app-lib
ui/workspace_tabs.cpp
ui/zoom_entry.cpp
ui_context.cpp
thumbnails.cpp
util/autocrop.cpp
util/clipboard.cpp
util/clipboard_native.cpp

View File

@ -0,0 +1,50 @@
// Aseprite
// Copyright (C) 2016 Carlo Caputo
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/app.h"
#include "app/commands/command.h"
#include "app/context.h"
#include "app/document.h"
#include "app/pref/preferences.h"
namespace app {
using namespace gfx;
class ToggleTimelineThumbnailsCommand : public Command {
public:
ToggleTimelineThumbnailsCommand()
: Command("ToggleTimelineThumbnails",
"Toggle Timeline Thumbnails",
CmdUIOnlyFlag)
{
}
Command* clone() const override { return new ToggleTimelineThumbnailsCommand(*this); }
protected:
bool onChecked(Context* context) override {
DocumentPreferences& docPref = Preferences::instance().document(context->activeDocument());
return docPref.thumbnails.enabled();
}
void onExecute(Context* context) override {
DocumentPreferences& docPref = Preferences::instance().document(context->activeDocument());
docPref.thumbnails.enabled(!docPref.thumbnails.enabled());
}
};
Command* CommandFactory::createToggleTimelineThumbnailsCommand()
{
return new ToggleTimelineThumbnailsCommand;
}
} // namespace app

View File

@ -122,6 +122,7 @@ FOR_EACH_COMMAND(ShowExtras)
FOR_EACH_COMMAND(ShowGrid)
FOR_EACH_COMMAND(ShowLayerEdges)
FOR_EACH_COMMAND(ShowOnionSkin)
FOR_EACH_COMMAND(ToggleTimelineThumbnails)
FOR_EACH_COMMAND(ShowPixelGrid)
FOR_EACH_COMMAND(ShowSelectionEdges)
FOR_EACH_COMMAND(SnapToGrid)

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2016 Carlo "zED" Caputo
// Copyright (C) 2016 Carlo Caputo
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as

View File

@ -26,6 +26,7 @@
#include "gfx/rect.h"
#include "render/onionskin_position.h"
#include "render/zoom.h"
#include "doc/algorithm/resize_image.h"
#include "pref.xml.h"

96
src/app/thumbnails.cpp Normal file
View File

@ -0,0 +1,96 @@
// Aseprite
// Copyright (C) 2016 Carlo Caputo
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
#include "app/color_utils.h"
#include "app/document.h"
#include "app/pref/preferences.h"
#include "app/thumbnails.h"
#include "base/bind.h"
#include "base/connection.h"
#include "base/shared_ptr.h"
#include "doc/algorithm/resize_image.h"
#include "doc/algorithm/rotate.h"
#include "doc/cel.h"
#include "doc/conversion_she.h"
#include "doc/doc.h"
#include "doc/frame.h"
#include "doc/object.h"
#include "doc/object_id.h"
#include "render/render.h"
#include "she/surface.h"
#include "she/system.h"
namespace app {
namespace thumb {
she::Surface* get_cel_thumbnail(const doc::Cel* cel, const gfx::Rect& bounds)
{
app::Document* document = static_cast<app::Document*>(cel->sprite()->document());
doc::frame_t frame = cel->frame();
doc::Image* image = cel->image();
DocumentPreferences& docPref = Preferences::instance().document(document);
int opacity = docPref.thumbnails.opacity();
gfx::Color background = color_utils::color_for_ui(docPref.thumbnails.background());
doc::algorithm::ResizeMethod resize_method = docPref.thumbnails.quality();
gfx::Size image_size = image->size();
gfx::Size thumb_size = bounds.size();
double zw = thumb_size.w / (double)image_size.w;
double zh = thumb_size.h / (double)image_size.h;
double zoom = MIN(1, MIN(zw, zh));
gfx::Rect cel_image_on_thumb(
(int)(thumb_size.w * 0.5 - image_size.w * zoom * 0.5),
(int)(thumb_size.h * 0.5 - image_size.h * zoom * 0.5),
(int)(image_size.w * zoom),
(int)(image_size.h * zoom)
);
const doc::Sprite* sprite = document->sprite();
base::UniquePtr<doc::Image> thumb_img(doc::Image::create(
image->pixelFormat(), thumb_size.w, thumb_size.h));
clear_image(thumb_img.get(), background);
base::UniquePtr<doc::Image> scale_img;
const doc::Image* source = image;
if (zoom != 1) {
scale_img.reset(doc::Image::create(
image->pixelFormat(), cel_image_on_thumb.w, cel_image_on_thumb.h));
doc::algorithm::resize_image(
image, scale_img,
resize_method,
sprite->palette(frame),
sprite->rgbMap(frame),
sprite->transparentColor());
source = scale_img.get();
}
render::composite_image(
thumb_img, source,
sprite->palette(frame),
cel_image_on_thumb.x,
cel_image_on_thumb.y,
opacity, BlendMode::NORMAL);
she::Surface* thumb_surf = she::instance()->createRgbaSurface(
thumb_img->width(), thumb_img->height());
convert_image_to_surface(thumb_img, sprite->palette(frame), thumb_surf,
0, 0, 0, 0, thumb_img->width(), thumb_img->height());
return thumb_surf;
}
} // thumb
} // app

30
src/app/thumbnails.h Normal file
View File

@ -0,0 +1,30 @@
// Aseprite
// Copyright (C) 2016 Carlo Caputo
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
#ifndef APP_THUMBNAILS_H_INCLUDED
#define APP_THUMBNAILS_H_INCLUDED
#pragma once
#include "gfx/rect.h"
namespace doc {
class Cel;
}
namespace she {
class Surface;
}
namespace app {
namespace thumb {
she::Surface* get_cel_thumbnail(const doc::Cel* cel, const gfx::Rect& bounds);
} // thumb
} // app
#endif

View File

@ -41,6 +41,8 @@ ConfigureTimelinePopup::ConfigureTimelinePopup()
: PopupWindow("Timeline Settings", ClickBehavior::CloseOnClickInOtherWindow)
, m_lockUpdates(false)
{
setHotRegion(gfx::Region(manager()->bounds())); // for the color selector
setAutoRemap(false);
setBorder(gfx::Border(4*guiscale()));
@ -56,6 +58,17 @@ ConfigureTimelinePopup::ConfigureTimelinePopup()
m_box->currentLayer()->Click.connect(base::Bind<void>(&ConfigureTimelinePopup::onCurrentLayerChange, this));
m_box->behind()->Click.connect(base::Bind<void>(&ConfigureTimelinePopup::onPositionChange, this));
m_box->infront()->Click.connect(base::Bind<void>(&ConfigureTimelinePopup::onPositionChange, this));
m_box->thumbOpacity()->Change.connect(base::Bind<void>(&ConfigureTimelinePopup::onThumbOpacityChange, this));
m_box->thumbBackground()->Change.connect(&ConfigureTimelinePopup::onThumbBackgroundChange, this);
m_box->thumbEnabled()->Click.connect(base::Bind<void>(&ConfigureTimelinePopup::onThumbEnabledChange, this));
m_box->thumbOverlayEnabled()->Click.connect(base::Bind<void>(&ConfigureTimelinePopup::onThumbOverlayEnabledChange, this));
m_box->thumbOverlaySize()->Change.connect(base::Bind<void>(&ConfigureTimelinePopup::onThumbOverlaySizeChange, this));
m_box->thumbQuality()->addItem("Nearest-neighbor");
m_box->thumbQuality()->addItem("Bilinear");
// m_box->thumbQuality()->addItem("RotSprite");
m_box->thumbQuality()->Change.connect(&ConfigureTimelinePopup::onThumbQualityChange, this);
}
app::Document* ConfigureTimelinePopup::doc()
@ -103,6 +116,24 @@ void ConfigureTimelinePopup::updateWidgetsFromCurrentSettings()
m_box->infront()->setSelected(true);
break;
}
switch (docPref.thumbnails.quality()) {
case doc::algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR:
m_box->thumbQuality()->setSelectedItemIndex(0);
break;
case doc::algorithm::RESIZE_METHOD_BILINEAR:
m_box->thumbQuality()->setSelectedItemIndex(1);
break;
default:
docPref.thumbnails.quality(doc::algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR);
m_box->thumbQuality()->setSelectedItemIndex(0);
}
m_box->thumbOpacity()->setValue(docPref.thumbnails.opacity());
m_box->thumbBackground()->setColor(docPref.thumbnails.background());
m_box->thumbEnabled()->setSelected(docPref.thumbnails.enabled());
m_box->thumbOverlayEnabled()->setSelected(docPref.thumbnails.overlayEnabled());
m_box->thumbOverlaySize()->setValue(docPref.thumbnails.overlaySize());
}
bool ConfigureTimelinePopup::onProcessMessage(ui::Message* msg)
@ -112,7 +143,6 @@ bool ConfigureTimelinePopup::onProcessMessage(ui::Message* msg)
case kOpenMessage: {
updateWidgetsFromCurrentSettings();
break;
}
}
return PopupWindow::onProcessMessage(msg);
@ -175,4 +205,45 @@ void ConfigureTimelinePopup::onPositionChange()
render::OnionskinPosition::INFRONT);
}
void ConfigureTimelinePopup::onThumbOpacityChange()
{
docPref().thumbnails.opacity(m_box->thumbOpacity()->getValue());
}
void ConfigureTimelinePopup::onThumbQualityChange()
{
switch (m_box->thumbQuality()->getSelectedItemIndex()) {
case 0:
docPref().thumbnails.quality(doc::algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR);
break;
case 1:
docPref().thumbnails.quality(doc::algorithm::RESIZE_METHOD_BILINEAR);
break;
default:
docPref().thumbnails.quality(doc::algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR);
m_box->thumbQuality()->setSelectedItemIndex(0);
}
}
void ConfigureTimelinePopup::onThumbBackgroundChange(const app::Color& color)
{
docPref().thumbnails.background(color);
}
void ConfigureTimelinePopup::onThumbEnabledChange()
{
docPref().thumbnails.enabled(m_box->thumbEnabled()->isSelected());
}
void ConfigureTimelinePopup::onThumbOverlayEnabledChange()
{
docPref().thumbnails.overlayEnabled(m_box->thumbOverlayEnabled()->isSelected());
}
void ConfigureTimelinePopup::onThumbOverlaySizeChange()
{
docPref().thumbnails.overlaySize(m_box->thumbOverlaySize()->getValue());
}
} // namespace app

View File

@ -12,6 +12,7 @@
#include "app/pref/preferences.h"
#include "doc/anidir.h"
#include "ui/popup_window.h"
#include "base/connection.h"
namespace ui {
class Button;
@ -41,6 +42,13 @@ namespace app {
void onCurrentLayerChange();
void onPositionChange();
void onThumbOpacityChange();
void onThumbQualityChange();
void onThumbBackgroundChange(const app::Color& color);
void onThumbEnabledChange();
void onThumbOverlayEnabledChange();
void onThumbOverlaySizeChange();
private:
void updateWidgetsFromCurrentSettings();
app::Document* doc();

View File

@ -50,6 +50,12 @@
#include "she/font.h"
#include "ui/scroll_helper.h"
#include "ui/ui.h"
#include "base/unique_ptr.h"
#include "she/surface.h"
#include "she/system.h"
#include "doc/conversion_she.h"
#include "base/bind.h"
#include "doc/algorithm/rotate.h"
#include <cstdio>
#include <vector>
@ -134,6 +140,8 @@ Timeline::Timeline()
, m_offset_count(0)
, m_scroll(false)
, m_fromTimeline(false)
, m_thumbnailsOverlayVisible(false)
, m_thumbnailsOverlayDirection((int)(FRMSIZE*1.5), (int)(FRMSIZE*0.5))
{
enableFlags(CTRL_RIGHT_CLICK);
@ -164,6 +172,11 @@ Timeline::~Timeline()
delete m_confPopup;
}
void Timeline::onThumbnailsPrefChange()
{
invalidate();
}
void Timeline::updateUsingEditor(Editor* editor)
{
m_aniControls.updateUsingEditor(editor);
@ -206,6 +219,10 @@ void Timeline::updateUsingEditor(Editor* editor)
m_hot.part = PART_NOTHING;
m_clk.part = PART_NOTHING;
m_thumbnailsPrefConn.disconnect();
m_thumbnailsPrefConn = docPref().thumbnails.AfterChange.connect(
base::Bind<void>(&Timeline::onThumbnailsPrefChange, this));
setFocusStop(true);
regenerateLayers();
setViewScroll(viewScroll());
@ -545,6 +562,7 @@ bool Timeline::onProcessMessage(Message* msg)
}
updateStatusBar(msg);
updateCelOverlayBounds(hit);
return true;
}
@ -1036,6 +1054,7 @@ void Timeline::onPaint(ui::PaintEvent& ev)
drawFrameTags(g);
drawRangeOutline(g);
drawClipboardRange(g);
drawCelOverlay(g);
#if 0 // Use this code to debug the calculated m_dropRange by updateDropRange()
{
@ -1067,8 +1086,10 @@ void Timeline::onAfterCommandExecution(CommandExecutionEvent& ev)
void Timeline::onRemoveDocument(doc::Document* document)
{
if (document == m_document)
if (document == m_document) {
m_thumbnailsPrefConn.disconnect();
detachDocument();
}
}
void Timeline::onGeneralUpdate(DocumentEvent& ev)
@ -1518,6 +1539,131 @@ void Timeline::drawCel(ui::Graphics* g, LayerIndex layerIndex, frame_t frame, Ce
// Draw decorators to link the activeCel with its links.
if (data->activeIt != data->end)
drawCelLinkDecorators(g, bounds, cel, frame, is_active, is_hover, data);
if (docPref().thumbnails.enabled() && image) {
gfx::Rect thumb_bounds = gfx::Rect(bounds).offset(1,1).inflate(-1,-1);
she::Surface* thumb_surf = thumb::get_cel_thumbnail(cel, thumb_bounds);
g->drawRgbaSurface(thumb_surf, thumb_bounds.x, thumb_bounds.y);
thumb_surf->dispose();
}
}
void Timeline::updateCelOverlayBounds(const Hit& hit)
{
gfx::Rect inner, outer;
if (docPref().thumbnails.overlayEnabled() && hit.part == PART_CEL) {
m_thumbnailsOverlayHit = hit;
int max_size = FRMSIZE * docPref().thumbnails.overlaySize();
int width, height;
if (m_sprite->width() > m_sprite->height()) {
width = max_size;
height = max_size * m_sprite->height() / m_sprite->width();
}
else {
width = max_size * m_sprite->width() / m_sprite->height();
height = max_size;
}
gfx::Rect client_bounds = clientBounds();
gfx::Point center = client_bounds.center();
gfx::Rect bounds_cel = getPartBounds(m_thumbnailsOverlayHit);
inner = gfx::Rect(
bounds_cel.x + m_thumbnailsOverlayDirection.x,
bounds_cel.y + m_thumbnailsOverlayDirection.y,
width,
height
);
if (!client_bounds.contains(inner)) {
m_thumbnailsOverlayDirection = gfx::Point(
bounds_cel.x < center.x ? (int)(FRMSIZE*1.5) : -width -(int)(FRMSIZE*0.5),
bounds_cel.y < center.y ? (int)(FRMSIZE*0.5) : -height+(int)(FRMSIZE*0.5)
);
inner.setOrigin(gfx::Point(
bounds_cel.x + m_thumbnailsOverlayDirection.x,
bounds_cel.y + m_thumbnailsOverlayDirection.y
));
}
outer = gfx::Rect(inner).enlarge(1);
}
else {
outer = gfx::Rect(0, 0, 0, 0);
}
if (outer != m_thumbnailsOverlayOuter) {
if (!m_thumbnailsOverlayOuter.isEmpty()) {
invalidateRect(gfx::Rect(m_thumbnailsOverlayOuter).offset(origin()));
}
if (!outer.isEmpty()) {
invalidateRect(gfx::Rect(outer).offset(origin()));
}
m_thumbnailsOverlayVisible = !outer.isEmpty();
m_thumbnailsOverlayOuter = outer;
m_thumbnailsOverlayInner = inner;
}
}
void Timeline::drawCelOverlay(ui::Graphics* g)
{
if (!m_thumbnailsOverlayVisible) {
return;
}
Layer *layer = m_layers[m_thumbnailsOverlayHit.layer];
Cel *cel = layer->cel(m_thumbnailsOverlayHit.frame);
if (!cel) {
return;
}
Image* image = cel->image();
if (!image) {
return;
}
IntersectClip clip(g, m_thumbnailsOverlayOuter);
if (!clip)
return;
base::UniquePtr<Image> overlay_img(
Image::create(image->pixelFormat(),
m_thumbnailsOverlayInner.w,
m_thumbnailsOverlayInner.h));
double scale = (
m_sprite->width() > m_sprite->height() ?
m_thumbnailsOverlayInner.w / (double)m_sprite->width() :
m_thumbnailsOverlayInner.h / (double)m_sprite->height()
);
clear_image(overlay_img, 0);
algorithm::scale_image(overlay_img, image,
(int)(cel->x() * scale),
(int)(cel->y() * scale),
(int)(image->width() * scale),
(int)(image->height() * scale),
0, 0, image->width(), image->height());
she::Surface* overlay_surf = she::instance()->createRgbaSurface(
overlay_img->width(),
overlay_img->height());
convert_image_to_surface(overlay_img, m_sprite->palette(m_frame), overlay_surf,
0, 0, 0, 0, overlay_img->width(), overlay_img->height());
gfx::Color background = color_utils::color_for_ui(docPref().thumbnails.background());
gfx::Color border = color_utils::blackandwhite_neg(background);
g->fillRect(background, m_thumbnailsOverlayInner);
g->drawRgbaSurface(overlay_surf,
m_thumbnailsOverlayInner.x, m_thumbnailsOverlayInner.y);
g->drawRect(border, m_thumbnailsOverlayOuter);
overlay_surf->dispose();
}
void Timeline::drawCelLinkDecorators(ui::Graphics* g, const gfx::Rect& bounds,

View File

@ -23,6 +23,7 @@
#include "ui/scroll_bar.h"
#include "ui/timer.h"
#include "ui/widget.h"
#include "app/thumbnails.h"
#include <vector>
@ -37,6 +38,10 @@ namespace ui {
class Graphics;
}
namespace she {
class Surface;
}
namespace app {
namespace skin {
@ -255,6 +260,10 @@ namespace app {
DocumentPreferences& docPref() const;
skin::SkinTheme* skinTheme() const;
void updateCelOverlayBounds(const Hit& hit);
void drawCelOverlay(ui::Graphics* g);
void onThumbnailsPrefChange();
ui::ScrollBar m_hbar;
ui::ScrollBar m_vbar;
gfx::Rect m_viewportArea;
@ -291,6 +300,13 @@ namespace app {
AniControls m_aniControls;
bool m_thumbnailsOverlayVisible;
gfx::Rect m_thumbnailsOverlayInner;
gfx::Rect m_thumbnailsOverlayOuter;
Hit m_thumbnailsOverlayHit;
gfx::Point m_thumbnailsOverlayDirection;
base::Connection m_thumbnailsPrefConn;
// Temporal data used to move the range.
struct MoveRange {
int activeRelativeLayer;