Add demo artwork

This commit is contained in:
Keavon Chambers 2023-08-22 03:26:59 -07:00
parent 0e97a256b7
commit 0dcfafbf64
33 changed files with 2133 additions and 1050 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -79,7 +79,7 @@ pub const DEFAULT_FONT_FAMILY: &str = "Merriweather";
pub const DEFAULT_FONT_STYLE: &str = "Normal (400)";
// Document
pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.17"; // Remember to save a simple document and replace the test file `graphite-test-document.graphite`
pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.17"; // Remember to update the demo artwork in /demos with both this version number and the contents so it remains editable
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
pub const FILE_SAVE_SUFFIX: &str = ".graphite";
pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences

View file

@ -555,7 +555,8 @@ mod test {
println!("-------------------------------------------------");
println!("Failed test due to receiving a DisplayDialogError while loading the Graphite sample file.");
println!("This is most likely caused by forgetting to bump the `GRAPHITE_DOCUMENT_VERSION` in `editor/src/consts.rs`");
println!("After bumping this version number, replace `graphite-test-document.graphite` with a valid file by saving a document from the editor.");
println!("After bumping this version number, update the documents in `/demo-artwork` by editing their JSON to");
println!("ensure they remain compatible with both the bumped version number and the serialization format change.");
println!("DisplayDialogError details:");
println!();
println!("Description: {}", value);
@ -567,19 +568,25 @@ mod test {
init_logger();
let mut editor = Editor::create();
let test_file = include_str!("../graphite-test-document.graphite");
let responses = editor.handle_message(PortfolioMessage::OpenDocumentFile {
document_name: "Graphite Version Test".into(),
document_serialized_content: test_file.into(),
});
let test_files = [
("Just a Potted Cactus", include_str!("../../demo-artwork/just-a-potted-cactus.graphite")),
("Valley of Spires", include_str!("../../demo-artwork/valley-of-spires.graphite")),
];
for response in responses {
// Check for the existence of the file format incompatibility warning dialog after opening the test file
if let FrontendMessage::UpdateDialogDetails { layout_target: _, diff } = response {
if let DiffUpdate::SubLayout(sub_layout) = &diff[0].new_value {
if let LayoutGroup::Row { widgets } = &sub_layout[0] {
if let Widget::TextLabel(TextLabel { value, .. }) = &widgets[0].widget {
print_problem_to_terminal_on_failure(value);
for (document_name, document_serialized_content) in test_files {
let responses = editor.handle_message(PortfolioMessage::OpenDocumentFile {
document_name: document_name.into(),
document_serialized_content: document_serialized_content.into(),
});
for response in responses {
// Check for the existence of the file format incompatibility warning dialog after opening the test file
if let FrontendMessage::UpdateDialogDetails { layout_target: _, diff } = response {
if let DiffUpdate::SubLayout(sub_layout) = &diff[0].new_value {
if let LayoutGroup::Row { widgets } = &sub_layout[0] {
if let Widget::TextLabel(TextLabel { value, .. }) = &widgets[0].widget {
print_problem_to_terminal_on_failure(value);
}
}
}
}

View file

@ -33,6 +33,7 @@ pub enum DialogMessage {
RequestComingSoonDialog {
issue: Option<i32>,
},
RequestDemoArtworkDialog,
RequestExportDialog,
RequestNewDocumentDialog,
RequestPreferencesDialog,

View file

@ -1,7 +1,8 @@
use super::simple_dialogs::{self, AboutGraphiteDialog, ComingSoonDialog};
use super::simple_dialogs::{self, AboutGraphiteDialog, ComingSoonDialog, DemoArtworkDialog};
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
/// Stores the dialogs which require state. These are the ones that have their own message handlers, and are not the ones defined in `simple_dialogs`.
#[derive(Debug, Default, Clone)]
pub struct DialogMessageHandler {
export_dialog: ExportDialogMessageHandler,
@ -53,6 +54,11 @@ impl MessageHandler<DialogMessage, (&PortfolioMessageHandler, &PreferencesMessag
coming_soon.send_layout(responses, LayoutTarget::DialogDetails);
responses.add(FrontendMessage::DisplayDialog { icon: "Warning".to_string() });
}
DialogMessage::RequestDemoArtworkDialog => {
let demo_artwork_dialog = DemoArtworkDialog;
demo_artwork_dialog.send_layout(responses, LayoutTarget::DialogDetails);
responses.add(FrontendMessage::DisplayDialog { icon: "Image".to_string() });
}
DialogMessage::RequestExportDialog => {
if let Some(document) = portfolio.active_document() {
let artboard_handler = &document.artboard_message_handler;

View file

@ -50,7 +50,7 @@ impl MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHa
impl LayoutHolder for NewDocumentDialogMessageHandler {
fn layout(&self) -> Layout {
let title = vec![TextLabel::new("New document").bold(true).widget_holder()];
let title = vec![TextLabel::new("New Document").bold(true).widget_holder()];
let name = vec![
TextLabel::new("Name").table_align(true).widget_holder(),

View file

@ -8,6 +8,7 @@ impl LayoutHolder for CloseAllDocumentsDialog {
fn layout(&self) -> Layout {
let discard = TextButton::new("Discard All")
.min_width(96)
.emphasized(true)
.on_update(|_| {
DialogMessage::CloseDialogAndThen {
followups: vec![PortfolioMessage::CloseAllDocuments.into()],

View file

@ -0,0 +1,50 @@
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
/// A dialog to let the user browse a gallery of demo artwork that can be opened.
pub struct DemoArtworkDialog;
impl LayoutHolder for DemoArtworkDialog {
fn layout(&self) -> Layout {
let artwork = [
(
"Valley of Spires",
"ThumbnailValleyOfSpires",
"https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/demo-artwork/valley-of-spires.graphite",
),
(
"Just a Potted Cactus",
"ThumbnailJustAPottedCactus",
"https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/demo-artwork/just-a-potted-cactus.graphite",
),
];
let image_widgets = artwork
.into_iter()
.map(|(_, thumbnail, _)| ImageLabel::new(thumbnail.to_string()).width(Some("256px".into())).widget_holder())
.collect();
let button_widgets = artwork
.into_iter()
.map(|(label, _, url)| {
TextButton::new(label)
.min_width(256)
.on_update(|_| {
DialogMessage::CloseDialogAndThen {
followups: vec![FrontendMessage::TriggerFetchAndOpenDocument { url: url.to_string() }.into()],
}
.into()
})
.widget_holder()
})
.collect();
Layout::WidgetLayout(WidgetLayout::new(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new("Demo Artwork".to_string()).bold(true).widget_holder()],
},
LayoutGroup::Row { widgets: image_widgets },
LayoutGroup::Row { widgets: button_widgets },
]))
}
}

View file

@ -2,10 +2,12 @@ mod about_graphite_dialog;
mod close_all_documents_dialog;
mod close_document_dialog;
mod coming_soon_dialog;
mod demo_artwork_dialog;
mod error_dialog;
pub use about_graphite_dialog::AboutGraphiteDialog;
pub use close_all_documents_dialog::CloseAllDocumentsDialog;
pub use close_document_dialog::CloseDocumentDialog;
pub use coming_soon_dialog::ComingSoonDialog;
pub use demo_artwork_dialog::DemoArtworkDialog;
pub use error_dialog::ErrorDialog;

View file

@ -67,6 +67,9 @@ pub enum FrontendMessage {
document: String,
name: String,
},
TriggerFetchAndOpenDocument {
url: String,
},
TriggerFontLoad {
font: Font,
#[serde(rename = "isDefault")]

View file

@ -158,6 +158,7 @@ impl<F: Fn(&MessageDiscriminant) -> Vec<KeysGroup>> MessageHandler<LayoutMessage
responses.add(callback_message);
}
Widget::IconLabel(_) => {}
Widget::ImageLabel(_) => {}
Widget::InvisibleStandinInput(invisible) => {
let callback_message = (invisible.on_update.callback)(&());
responses.add(callback_message);

View file

@ -288,6 +288,7 @@ impl LayoutGroup {
Widget::FontInput(x) => &mut x.tooltip,
Widget::IconButton(x) => &mut x.tooltip,
Widget::IconLabel(x) => &mut x.tooltip,
Widget::ImageLabel(x) => &mut x.tooltip,
Widget::LayerReferenceInput(x) => &mut x.tooltip,
Widget::NumberInput(x) => &mut x.tooltip,
Widget::OptionalInput(x) => &mut x.tooltip,
@ -438,6 +439,7 @@ pub enum Widget {
FontInput(FontInput),
IconButton(IconButton),
IconLabel(IconLabel),
ImageLabel(ImageLabel),
InvisibleStandinInput(InvisibleStandinInput),
LayerReferenceInput(LayerReferenceInput),
NumberInput(NumberInput),
@ -516,6 +518,7 @@ impl DiffUpdate {
Widget::PopoverButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::IconLabel(_)
| Widget::ImageLabel(_)
| Widget::CurveInput(_)
| Widget::InvisibleStandinInput(_)
| Widget::PivotAssist(_)

View file

@ -12,6 +12,18 @@ pub struct IconLabel {
pub tooltip: String,
}
#[derive(Clone, Serialize, Deserialize, Derivative, Debug, Default, PartialEq, Eq, WidgetBuilder, specta::Type)]
pub struct ImageLabel {
#[widget_builder(constructor)]
pub image: String,
pub width: Option<String>,
pub height: Option<String>,
pub tooltip: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, WidgetBuilder, specta::Type)]
pub struct Separator {
pub direction: SeparatorDirection,

View file

@ -49,10 +49,17 @@ impl LayoutHolder for MenuBarMessageHandler {
},
MenuBarEntry {
label: "Open…".into(),
icon: Some("Folder".into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::OpenDocument),
action: MenuBarEntry::create_action(|_| PortfolioMessage::OpenDocument.into()),
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Open Demo Artwork…".into(),
icon: Some("Image".into()),
action: MenuBarEntry::create_action(|_| DialogMessage::RequestDemoArtworkDialog.into()),
..MenuBarEntry::default()
},
],
vec![
MenuBarEntry {

View file

@ -1,12 +1,21 @@
{
"extends": "@parcel/config-default",
"transformers": {
"*.svelte": [
"parcel-transformer-svelte3-plus"
],
"*.svg": [
"...",
"@parcel/transformer-inline-string"
],
"*.svelte": [
"parcel-transformer-svelte3-plus"
"*.png, *.jpg": [
"...",
"@parcel/transformer-inline-string"
]
},
"optimizers": {
"*.png, *.jpg": [
"@parcel/optimizer-data-url"
]
}
}

View file

Before

Width:  |  Height:  |  Size: 189 B

After

Width:  |  Height:  |  Size: 189 B

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because it is too large Load diff

View file

@ -34,17 +34,18 @@
"reflect-metadata": "^0.1.13"
},
"devDependencies": {
"@parcel/config-default": "^2.8.3",
"@parcel/packager-raw-url": "^2.8.3",
"@parcel/transformer-inline-string": "^2.8.3",
"@parcel/transformer-webmanifest": "^2.8.3",
"@parcel/config-default": "^2.9.3",
"@parcel/packager-raw-url": "^2.9.3",
"@parcel/optimizer-data-url": "^2.9.3",
"@parcel/transformer-inline-string": "^2.9.3",
"@parcel/transformer-webmanifest": "^2.9.3",
"@types/license-checker-webpack-plugin": "^0.2.1",
"@types/node": "^18.16.2",
"@types/webpack": "^5.28.1",
"buffer": "^5.7.1",
"concurrently": "^8.0.1",
"license-checker-webpack-plugin": "^0.2.1",
"parcel": "^2.8.3",
"parcel": "^2.9.3",
"parcel-transformer-svelte3-plus": "^0.2.9",
"postcss": "^8.4.23",
"process": "^0.11.10",

View file

@ -84,6 +84,10 @@
height: auto;
}
.image-label {
border-radius: 2px;
}
.panic-buttons-row {
height: 32px;
align-items: center;

View file

@ -24,6 +24,7 @@
import TextAreaInput from "@graphite/components/widgets/inputs/TextAreaInput.svelte";
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import ImageLabel from "@graphite/components/widgets/labels/ImageLabel.svelte";
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
import { getContext } from "svelte";
@ -120,6 +121,10 @@
{#if iconLabel}
<IconLabel {...exclude(iconLabel)} />
{/if}
{@const imageLabel = narrowWidgetProps(component.props, "ImageLabel")}
{#if imageLabel}
<ImageLabel {...exclude(imageLabel)} />
{/if}
{@const layerReferenceInput = narrowWidgetProps(component.props, "LayerReferenceInput")}
{#if layerReferenceInput}
<LayerReferenceInput {...exclude(layerReferenceInput)} on:value={({ detail }) => updateLayout(index, detail)} />

View file

@ -0,0 +1,29 @@
<script lang="ts">
import { IMAGE_BASE64_STRINGS } from "@graphite/utility-functions/images";
let className = "";
export { className as class };
export let classes: Record<string, boolean> = {};
export let image: string;
export let width: string | undefined;
export let height: string | undefined;
export let tooltip: string | undefined = undefined;
$: extraClasses = Object.entries(classes)
.flatMap((classAndState) => (classAndState[1] ? [classAndState[0]] : []))
.join(" ");
</script>
<img src={IMAGE_BASE64_STRINGS[image]} style:width style:height class={`image-label ${className} ${extraClasses}`.trim()} title={tooltip} alt="" />
<style lang="scss" global>
.image-label {
width: auto;
height: auto;
+ .image-label {
margin-left: 8px;
}
}
</style>

View file

@ -40,14 +40,6 @@
let tabElements: (LayoutRow | undefined)[] = [];
function newDocument() {
editor.instance.newDocumentDialog();
}
function openDocument() {
editor.instance.documentOpen();
}
function platformModifiers(reservedKey: boolean): LayoutKeysGroup {
// TODO: Remove this by properly feeding these keys from a layout provided by the backend
@ -128,7 +120,7 @@
<table>
<tr>
<td>
<TextButton label="New Document" icon="File" action={() => newDocument()} />
<TextButton label="New Document" icon="File" action={() => editor.instance.newDocumentDialog()} />
</td>
<td>
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(true), { key: "KeyN", label: "N" }]]} />
@ -136,12 +128,17 @@
</tr>
<tr>
<td>
<TextButton label="Open Document" icon="Folder" action={() => openDocument()} />
<TextButton label="Open Document" icon="Folder" action={() => editor.instance.openDocument()} />
</td>
<td>
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(false), { key: "KeyO", label: "O" }]]} />
</td>
</tr>
<tr>
<td colspan="2">
<TextButton label="Open Demo Artwork" icon="Image" action={() => editor.instance.demoArtworkDialog()} />
</td>
</tr>
</table>
</LayoutRow>
</LayoutCol>

View file

@ -5,3 +5,13 @@ declare module "*.svg" {
const content: string;
export default content;
}
declare module "*.png" {
const content: string;
export default content;
}
declare module "*.jpg" {
const content: string;
export default content;
}

View file

@ -8,10 +8,10 @@ import { type Editor } from "@graphite/wasm-communication/editor";
import {
type FrontendDocumentDetails,
TriggerCopyToClipboardBlobUrl,
TriggerFetchAndOpenDocument,
TriggerDownloadBlobUrl,
TriggerDownloadRaster,
TriggerDownloadTextFile,
TriggerImaginateCheckServerStatus,
TriggerImport,
TriggerOpenDocument,
TriggerRasterizeRegionBelowLayer,
@ -45,6 +45,19 @@ export function createPortfolioState(editor: Editor) {
return state;
})
});
editor.subscriptions.subscribeJsMessage(TriggerFetchAndOpenDocument, async (triggerFetchAndOpenDocument) => {
try {
const url = new URL(triggerFetchAndOpenDocument.url);
const data = await fetch(url);
const filename = url.pathname.split("/").pop() || "Untitled";
const content = await data.text();
editor.instance.openDocumentFile(filename, content);
} catch {
editor.instance.errorDialog("Failed to open document", "The file could not be reached over the internet. You may be offline, or it may be missing.");
}
});
editor.subscriptions.subscribeJsMessage(TriggerOpenDocument, async () => {
const extension = editor.instance.fileSaveSuffix();
const data = await upload(extension, "text");

View file

@ -113,7 +113,7 @@ import NodeBlur from "@graphite-frontend/assets/icon-16px-solid/node-blur.svg";
import NodeBrushwork from "@graphite-frontend/assets/icon-16px-solid/node-brushwork.svg";
import NodeColorCorrection from "@graphite-frontend/assets/icon-16px-solid/node-color-correction.svg";
import NodeGradient from "@graphite-frontend/assets/icon-16px-solid/node-gradient.svg";
import NodeImage from "@graphite-frontend/assets/icon-16px-solid/node-image.svg";
import Image from "@graphite-frontend/assets/icon-16px-solid/image.svg";
import NodeImaginate from "@graphite-frontend/assets/icon-16px-solid/node-imaginate.svg";
import NodeMagicWand from "@graphite-frontend/assets/icon-16px-solid/node-magic-wand.svg";
import NodeMask from "@graphite-frontend/assets/icon-16px-solid/node-mask.svg";
@ -176,7 +176,7 @@ const SOLID_16PX = {
NodeBrushwork: { svg: NodeBrushwork, size: 16 },
NodeColorCorrection: { svg: NodeColorCorrection, size: 16 },
NodeGradient: { svg: NodeGradient, size: 16 },
NodeImage: { svg: NodeImage, size: 16 },
Image: { svg: Image, size: 16 },
NodeImaginate: { svg: NodeImaginate, size: 16 },
NodeMagicWand: { svg: NodeMagicWand, size: 16 },
NodeMask: { svg: NodeMask, size: 16 },

View file

@ -0,0 +1,23 @@
/* eslint-disable import/first */
// Demo artwork
import ThumbnailJustAPottedCactus from "@graphite-frontend/assets/images/demo-artwork/thumbnail-just-a-potted-cactus.png";
import ThumbnailValleyOfSpires from "@graphite-frontend/assets/images/demo-artwork/thumbnail-valley-of-spires.png";
const DEMO_ARTWORK = {
ThumbnailJustAPottedCactus,
ThumbnailValleyOfSpires,
} as const;
// All images
const IMAGE_LIST = {
...DEMO_ARTWORK,
} as const;
// Exported images and types
export const IMAGES: ImageDefinitionType<typeof IMAGE_LIST> = IMAGE_LIST;
export const IMAGE_BASE64_STRINGS = Object.fromEntries(Object.entries(IMAGES).map(([name, data]) => [name, data]));
// See `icons.ts` for explanation about how this works
type EvaluateType<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
type ImageDefinitionType<T extends Record<string, string>> = EvaluateType<{ [key in keyof T]: string }>;

View file

@ -89,6 +89,21 @@ export function createEditor() {
// Subscriptions: Allows subscribing to messages in JS that are sent from the WASM backend
const subscriptions: SubscriptionRouter = createSubscriptionRouter();
// Check if the URL hash fragment has any demo artwork to be loaded
(async () => {
const demoArtwork = window.location.hash.trim().match(/#demo\/(.*)/)?.[1];
if (!demoArtwork) return;
try {
const url = new URL(`https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/demo-artwork/${demoArtwork}.graphite`);
const data = await fetch(url);
const filename = url.pathname.split("/").pop() || "Untitled";
const content = await data.text();
instance.openDocumentFile(filename, content);
} catch {}
})();
return {
raw,
instance,

View file

@ -513,6 +513,10 @@ export class TriggerLoadAutoSaveDocuments extends JsMessage { }
export class TriggerLoadPreferences extends JsMessage { }
export class TriggerFetchAndOpenDocument extends JsMessage {
readonly url!: string;
}
export class TriggerOpenDocument extends JsMessage { }
export class TriggerImport extends JsMessage { }
@ -874,6 +878,19 @@ export class IconLabel extends WidgetProps {
tooltip!: string | undefined;
}
export class ImageLabel extends WidgetProps {
image!: IconName;
@Transform(({ value }: { value: string }) => value || undefined)
width!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
height!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
export class LayerReferenceInput extends WidgetProps {
@Transform(({ value }: { value: BigUint64Array | undefined }) => (value ? String(value) : undefined))
value!: string | undefined;
@ -1120,6 +1137,7 @@ const widgetSubTypes = [
{ value: FontInput, name: "FontInput" },
{ value: IconButton, name: "IconButton" },
{ value: IconLabel, name: "IconLabel" },
{ value: ImageLabel, name: "ImageLabel" },
{ value: LayerReferenceInput, name: "LayerReferenceInput" },
{ value: NumberInput, name: "NumberInput" },
{ value: OptionalInput, name: "OptionalInput" },
@ -1367,6 +1385,7 @@ export const messageMakers: Record<string, MessageMaker> = {
DisplayRemoveEditableTextbox,
TriggerAboutGraphiteLocalizedCommitDate,
TriggerCopyToClipboardBlobUrl,
TriggerFetchAndOpenDocument,
TriggerDownloadBlobUrl,
TriggerDownloadRaster,
TriggerDownloadTextFile,

View file

@ -308,12 +308,18 @@ impl JsEditorHandle {
self.dispatch(message);
}
#[wasm_bindgen(js_name = documentOpen)]
pub fn document_open(&self) {
#[wasm_bindgen(js_name = openDocument)]
pub fn open_document(&self) {
let message = PortfolioMessage::OpenDocument;
self.dispatch(message);
}
#[wasm_bindgen(js_name = demoArtworkDialog)]
pub fn demo_artwork_dialog(&self) {
let message = DialogMessage::RequestDemoArtworkDialog;
self.dispatch(message);
}
#[wasm_bindgen(js_name = openDocumentFile)]
pub fn open_document_file(&self, document_name: String, document_serialized_content: String) {
let message = PortfolioMessage::OpenDocumentFile {