Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/og/sharable-purchase/moon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
@use "pages/shop/checkout";
@use "pages/shop/order";
@use "pages/shop/my_orders";
@use "pages/shop/sharable_purchase";
@use "pages/shop/admin_order_show";
@use "pages/user/show" as users_show;
@use "pages/profile_placeholder";
Expand Down
68 changes: 68 additions & 0 deletions app/assets/stylesheets/pages/shop/_sharable_purchase.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
.sharable-purchase {
display: flex;
align-items: center;
gap: 1.5rem;
margin-bottom: 3rem;
padding: 1.5rem;
background: var(--color-space-card, rgba(255, 255, 255, 0.04));
border: 1px solid var(--color-space-border, rgba(255, 255, 255, 0.12));
border-radius: 14px;

&__preview {
flex: none;
width: min(260px, 40%);
}

&__image {
display: block;
width: 100%;
height: auto;
border-radius: 10px;
border: 1px solid var(--color-space-border, rgba(255, 255, 255, 0.12));
}

&__content {
display: flex;
flex-direction: column;
gap: 0.75rem;
min-width: 0;
}

&__title {
margin: 0;
font-size: var(--font-size-xl);
color: var(--color-space-text);

em {
font-family: var(--font-family-subtitle);
color: var(--color-brand-yellow);
}
}

&__hint {
margin: 0;
color: var(--color-space-text-muted, var(--color-space-text));
}

&__actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}

&__status {
margin: 0;
min-height: 1.2em;
font-size: var(--font-size-s);
color: var(--color-brand-mint);
}

@media (max-width: 640px) {
flex-direction: column;
align-items: stretch;

&__preview {
width: 100%;
}
}
}
17 changes: 17 additions & 0 deletions app/controllers/shop/orders/flex_images_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class Shop::Orders::FlexImagesController < Shop::BaseController
def show
head :not_found and return unless Flipper.enabled?(:sharable_purchase, current_user)

authorize :shop, :flex_image?
order = current_user.shop_orders.find(params[:order_id])

png_data = OgImage::ShopOrderFlex.new(order).to_png

expires_in 30.minutes, public: false
if params[:download].present?
send_data png_data, type: "image/png", disposition: "attachment", filename: "stardance-order-#{order.id}.png"
else
send_data png_data, type: "image/png", disposition: "inline"
end
end
end
11 changes: 11 additions & 0 deletions app/controllers/shop/orders_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ def index
.where(parent_order_id: nil)
.includes(accessory_orders: { shop_item: { image_attachment: :blob } }, shop_item: { image_attachment: :blob })
.order(id: :desc)
@sharable_order = find_sharable_order
end

def create
Expand Down Expand Up @@ -195,6 +196,16 @@ def cancel

private

def find_sharable_order
return nil unless Flipper.enabled?(:sharable_purchase, current_user)

latest = @orders.worth_counting.first
return nil unless latest && latest.created_at > 10.minutes.ago
return nil if latest.shop_item.is_a?(ShopItem::TutorialNothing)

latest
end

def fulfill_free_stickers!
@shop_item.fulfill!(@order)
@order.mark_stickers_received
Expand Down
3 changes: 3 additions & 0 deletions app/javascript/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,9 @@ application.register("scroll-restore", ScrollRestoreController);
import SearchableSelectController from "./searchable_select_controller";
application.register("searchable-select", SearchableSelectController);

import SharePurchaseController from "./share_purchase_controller";
application.register("share-purchase", SharePurchaseController);

import ShopController from "./shop_controller";
application.register("shop", ShopController);

Expand Down
44 changes: 44 additions & 0 deletions app/javascript/controllers/share_purchase_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
static values = { url: String };
static targets = ["status"];

async copy(event) {
event.preventDefault();
try {
if (
!navigator.clipboard ||
!window.ClipboardItem ||
!window.isSecureContext
) {
throw new Error("clipboard unsupported");
}
// Pass the promise straight to ClipboardItem so Safari keeps the
// user-gesture context while the PNG downloads.
const item = new ClipboardItem({
"image/png": fetch(this.urlValue).then((response) => {
if (!response.ok) throw new Error("image fetch failed");
return response.blob();
}),
});
await navigator.clipboard.write([item]);
this.#status("Copied! Paste it anywhere.");
} catch {
this.#status("Couldn't copy on this browser — use Download PNG instead.");
}
}

#status(message) {
if (!this.hasStatusTarget) return;
this.statusTarget.textContent = message;
clearTimeout(this.statusTimeout);
this.statusTimeout = setTimeout(() => {
this.statusTarget.textContent = "";
}, 4000);
}

disconnect() {
clearTimeout(this.statusTimeout);
}
}
4 changes: 4 additions & 0 deletions app/policies/shop_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ def cancel?
signed_in_any?
end

def flex_image?
signed_in_any?
end

def destroy?
signed_in_any?
end
Expand Down
46 changes: 28 additions & 18 deletions app/services/og_image/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ def to_png

protected

# Canvas dimensions resolve through the subclass so a renderer can
# declare its own WIDTH/HEIGHT constants (e.g. a square canvas).
def canvas_width
self.class::WIDTH
end

def canvas_height
self.class::HEIGHT
end

def draw_rounded_rect(x:, y:, width:, height:, radius: 24, fill: "#ffffff", fill_opacity: 1.0, stroke: nil, stroke_width: 0)
r, g, b = hex_to_rgb(fill)
rect = rounded_rect_mask(width, height, radius)
Expand All @@ -85,10 +95,10 @@ def create_patterned_canvas(
fr, fg, fb = hex_to_rgb(frame_color)
cr, cg, cb = hex_to_rgb(card_color)

canvas = solid_rgba(WIDTH, HEIGHT, fr, fg, fb)
canvas = solid_rgba(canvas_width, canvas_height, fr, fg, fb)

cw = WIDTH - inset * 2
ch = HEIGHT - inset * 2
cw = canvas_width - inset * 2
ch = canvas_height - inset * 2
card_mask = rounded_rect_mask(cw, ch, card_radius)
card = solid_rgba(cw, ch, cr, cg, cb)
card = card.extract_band(0, n: 3).bandjoin(card_mask)
Expand All @@ -97,10 +107,10 @@ def create_patterned_canvas(
pattern_path = Rails.root.join("app", "assets", "images", "mask", "pattern.png").to_s
if File.exist?(pattern_path)
pattern = Vips::Image.new_from_file(pattern_path)
pattern = pattern.resize(WIDTH.to_f / pattern.width, vscale: HEIGHT.to_f / pattern.height)
pattern = pattern.resize(canvas_width.to_f / pattern.width, vscale: canvas_height.to_f / pattern.height)

pat_rgb = pattern.extract_band(0, n: 3)
pat_alpha = pattern.bands >= 4 ? pattern.extract_band(3) : Vips::Image.black(WIDTH, HEIGHT).new_from_image(255).cast(:uchar)
pat_alpha = pattern.bands >= 4 ? pattern.extract_band(3) : Vips::Image.black(canvas_width, canvas_height).new_from_image(255).cast(:uchar)

canvas_rgb = canvas.extract_band(0, n: 3)
canvas_alpha = canvas.extract_band(3)
Expand Down Expand Up @@ -169,10 +179,10 @@ def create_stardance_canvas(
br, bg, bb = hex_to_rgb(bg_color)
cr, cg, cb = hex_to_rgb(card_color)

canvas = solid_rgba(WIDTH, HEIGHT, br, bg, bb)
canvas = solid_rgba(canvas_width, canvas_height, br, bg, bb)

cw = WIDTH - inset * 2
ch = HEIGHT - inset * 2
cw = canvas_width - inset * 2
ch = canvas_height - inset * 2
card_mask = rounded_rect_mask(cw, ch, card_radius)
card = solid_rgba(cw, ch, cr, cg, cb)
card = card.extract_band(0, n: 3).bandjoin(card_mask)
Expand All @@ -181,7 +191,7 @@ def create_stardance_canvas(
nebula_path = Rails.root.join("app", "assets", "images", "landing", "how-this-works", "nebula-bg.png").to_s
if File.exist?(nebula_path)
nebula = Vips::Image.new_from_file(nebula_path)
nebula = nebula.resize(WIDTH.to_f / nebula.width, vscale: HEIGHT.to_f / nebula.height)
nebula = nebula.resize(canvas_width.to_f / nebula.width, vscale: canvas_height.to_f / nebula.height)
nebula = ensure_four_bands(nebula)
neb_rgb = nebula.extract_band(0, n: 3)
neb_alpha = nebula.extract_band(3)
Expand Down Expand Up @@ -283,14 +293,14 @@ def place_star_character(x: 60, y: 60, width: 220, height: 220, gravity: "SouthE

def draw_diagonal_scrim(opacity: 0.55)
r, g, b = hex_to_rgb("#08061e")
h_ramp = Vips::Image.identity(bands: 1).resize(WIDTH / 256.0, vscale: 1.0)
h_ramp = h_ramp.linear(-1.0, 255.0).resize(1, vscale: HEIGHT.to_f)
h_ramp = Vips::Image.identity(bands: 1).resize(canvas_width / 256.0, vscale: 1.0)
h_ramp = h_ramp.linear(-1.0, 255.0).resize(1, vscale: canvas_height.to_f)

v_ramp = Vips::Image.identity(bands: 1).resize(1, vscale: HEIGHT / 256.0)
v_ramp = v_ramp.resize(WIDTH.to_f, vscale: 1.0)
v_ramp = Vips::Image.identity(bands: 1).resize(1, vscale: canvas_height / 256.0)
v_ramp = v_ramp.resize(canvas_width.to_f, vscale: 1.0)

diag = ((h_ramp + v_ramp) / 2.0 * opacity).cast(:uchar)
scrim = solid_rgba(WIDTH, HEIGHT, r, g, b).extract_band(0, n: 3).bandjoin(diag).copy(interpretation: :srgb)
scrim = solid_rgba(canvas_width, canvas_height, r, g, b).extract_band(0, n: 3).bandjoin(diag).copy(interpretation: :srgb)
@image = image.composite(scrim, :over, x: [ 0 ], y: [ 0 ])
end

Expand Down Expand Up @@ -384,13 +394,13 @@ def apply_gravity(gravity, x, y, obj_width, obj_height)
when "NorthWest"
[ x, y ]
when "NorthEast"
[ WIDTH - x - obj_width, y ]
[ canvas_width - x - obj_width, y ]
when "SouthWest"
[ x, HEIGHT - y - obj_height ]
[ x, canvas_height - y - obj_height ]
when "SouthEast"
[ WIDTH - x - obj_width, HEIGHT - y - obj_height ]
[ canvas_width - x - obj_width, canvas_height - y - obj_height ]
when "Center"
[ (WIDTH - obj_width) / 2 + x, (HEIGHT - obj_height) / 2 + y ]
[ (canvas_width - obj_width) / 2 + x, (canvas_height - obj_height) / 2 + y ]
else
[ x, y ]
end
Expand Down
3 changes: 2 additions & 1 deletion app/services/og_image/preview.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ module Preview
OgImage::User,
OgImage::Devlog,
OgImage::Missions,
OgImage::Shop
OgImage::Shop,
OgImage::ShopOrderFlex
].freeze

class << self
Expand Down
Loading
Loading