up seafile
This commit is contained in:
+12
-1
@@ -425,7 +425,18 @@ func (a *App) SendEQSL(qsoID int64, templateID int64, jpegB64 string) error {
|
|||||||
applog.Printf("qsl: send eQSL to %s (%s) failed: %v", to, q.Callsign, err)
|
applog.Printf("qsl: send eQSL to %s (%s) failed: %v", to, q.Callsign, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := a.qso.MarkEQSLSent(a.ctx, qsoID, time.Now().UTC().Format("20060102")); err != nil {
|
// Stamp the standard ADIF eqsl_sent flag plus an app-specific timestamp of
|
||||||
|
// the eQSL-card e-mail (APP_OPSLOG_QSL_SENT) — distinct from eqsl_sent, which
|
||||||
|
// an eQSL.cc upload may also set. q came straight from GetByID, so a full
|
||||||
|
// Update rewrites the row unchanged apart from these fields.
|
||||||
|
now := time.Now().UTC()
|
||||||
|
q.EQSLSent = "Y"
|
||||||
|
q.EQSLSentDate = now.Format("20060102")
|
||||||
|
if q.Extras == nil {
|
||||||
|
q.Extras = map[string]string{}
|
||||||
|
}
|
||||||
|
q.Extras["APP_OPSLOG_QSL_SENT"] = now.Format(time.RFC3339)
|
||||||
|
if err := a.qso.Update(a.ctx, q); err != nil {
|
||||||
applog.Printf("qsl: eQSL sent to %s but marking failed: %v", q.Callsign, err)
|
applog.Printf("qsl: eQSL sent to %s but marking failed: %v", q.Callsign, err)
|
||||||
return fmt.Errorf("eQSL sent but status not saved: %w", err)
|
return fmt.Errorf("eQSL sent but status not saved: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,12 @@
|
|||||||
// back to front: halo, drop shadow, outline, bevel base, bevel light edge,
|
// back to front: halo, drop shadow, outline, bevel base, bevel light edge,
|
||||||
// gradient face, wavy gloss band clipped to the glyphs.
|
// gradient face, wavy gloss band clipped to the glyphs.
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import type { RenderModel, CardElement, StyleParams, QSOBox } from './qslTypes';
|
import type { RenderModel, CardElement, StyleParams, QSOBox } from './qslTypes';
|
||||||
import { QSO_FIELD_LABELS } from './qslTypes';
|
import { QSO_FIELD_LABELS } from './qslTypes';
|
||||||
import type { CardAssets } from './qslAssets';
|
import type { CardAssets } from './qslAssets';
|
||||||
import { FONT_WEIGHTS } from './qslAssets';
|
import { FONT_WEIGHTS } from './qslAssets';
|
||||||
|
import { renderTextFx, type TextFxParams, type TextFxKind, type TextFxResult } from './textFx';
|
||||||
|
|
||||||
// An editor selection: an element index or the QSO box.
|
// An editor selection: an element index or the QSO box.
|
||||||
export type CardSelection = number | 'box' | null;
|
export type CardSelection = number | 'box' | null;
|
||||||
@@ -46,6 +47,49 @@ function fontSpec(font?: string): { family: string; weight: number | 'normal' }
|
|||||||
return { family: `'${font}'`, weight: FONT_WEIGHTS[font] ?? 'normal' };
|
return { family: `'${font}'`, weight: FONT_WEIGHTS[font] ?? 'normal' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Presets whose call text is rendered with the ported canvas FX (the only way
|
||||||
|
// to get the exact glossy/western look). Everything else stays plain SVG text.
|
||||||
|
const FX_GLOSSY = new Set(['gel_gold', 'gel_silver']);
|
||||||
|
const FX_WESTERN = new Set(['gel_gold_grunge', 'gel_silver_grunge']);
|
||||||
|
function fxKindFor(preset?: string): TextFxKind | null {
|
||||||
|
if (preset && FX_GLOSSY.has(preset)) return 'glossy';
|
||||||
|
if (preset && FX_WESTERN.has(preset)) return 'western';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function buildFxParams(e: CardElement): TextFxParams | null {
|
||||||
|
const kind = fxKindFor(e.style_preset);
|
||||||
|
if (!kind || !e.size) return null;
|
||||||
|
const fs = fontSpec(e.font);
|
||||||
|
const sp = e.style_params ?? {};
|
||||||
|
const fx = sp.fx ?? {};
|
||||||
|
const grad = sp.gradient ?? (kind === 'western' ? ['#FFC83A', '#F0A21E', '#D87A00'] : ['#ffe22d', '#ffd600', '#ffcc00']);
|
||||||
|
const silver = e.style_preset?.includes('silver');
|
||||||
|
return {
|
||||||
|
kind,
|
||||||
|
text: e.text ?? '',
|
||||||
|
weight: String(fs.weight === 'normal' ? 400 : fs.weight),
|
||||||
|
family: fs.family,
|
||||||
|
size: e.size,
|
||||||
|
space: e.size * (kind === 'western' ? 0.08 : 0.04),
|
||||||
|
cTop: grad[0], cMid: grad[1] ?? grad[0], cBot: grad[2] ?? grad[1] ?? grad[0],
|
||||||
|
// Dark inter-letter edge — fixed near-black (the navy outline adaptStyle
|
||||||
|
// sets is for the old SVG stack, not this look).
|
||||||
|
cDark: kind === 'western' ? '#1c130a' : '#262630',
|
||||||
|
cOuter: silver ? '#e8edf2' : '#ced3db',
|
||||||
|
// Per-call overrides from the editor (undefined → renderer default).
|
||||||
|
plump: fx.plump, edge: fx.edge, outerw: fx.outerw, gloss: fx.gloss,
|
||||||
|
glossH: fx.gloss_h, glossI: fx.gloss_i, innerB: fx.inner_b,
|
||||||
|
depth: fx.depth, angle: fx.angle, slant: fx.slant, grunge: fx.grunge,
|
||||||
|
bevel: fx.bevel, seed: fx.seed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Stable signature of an element's FX-relevant fields, so dragging (x/y only)
|
||||||
|
// doesn't trigger an expensive re-render of the canvas text.
|
||||||
|
function fxSignature(e: CardElement): string {
|
||||||
|
if (!fxKindFor(e.style_preset)) return '';
|
||||||
|
return [e.text, e.font, e.size, e.style_preset, (e.style_params?.gradient ?? []).join(','), JSON.stringify(e.style_params?.fx ?? {})].join('|');
|
||||||
|
}
|
||||||
|
|
||||||
function elementTransform(e: CardElement): string {
|
function elementTransform(e: CardElement): string {
|
||||||
const r = e.rotate ? ` rotate(${e.rotate})` : '';
|
const r = e.rotate ? ` rotate(${e.rotate})` : '';
|
||||||
return `translate(${e.x} ${e.y})${r}`;
|
return `translate(${e.x} ${e.y})${r}`;
|
||||||
@@ -295,10 +339,34 @@ export function CardPreview({ model, assets, width, selected, onSelect, onMove,
|
|||||||
const drag = useRef<{ sel: Exclude<CardSelection, null>; sx: number; sy: number; ox: number; oy: number } | null>(null);
|
const drag = useRef<{ sel: Exclude<CardSelection, null>; sx: number; sy: number; ox: number; oy: number } | null>(null);
|
||||||
const measureCanvas = useRef<HTMLCanvasElement | null>(null);
|
const measureCanvas = useRef<HTMLCanvasElement | null>(null);
|
||||||
|
|
||||||
// Measure the selected group for the dashed selection outline. For text we
|
// Embedded fonts may still be loading when the card first mounts; the canvas
|
||||||
// use the glyphs' real ink metrics — getBBox would span the font's full
|
// FX needs them for correct metrics, so (re)render once they're ready.
|
||||||
// ascent/descent, leaving a tall dead band above the letters and a frame
|
const [fontsReady, setFontsReady] = useState(false);
|
||||||
// that doesn't hug the call.
|
useEffect(() => {
|
||||||
|
let alive = true;
|
||||||
|
const ready = (document as Document & { fonts?: FontFaceSet }).fonts?.ready ?? Promise.resolve();
|
||||||
|
void ready.then(() => { if (alive) setFontsReady(true); });
|
||||||
|
return () => { alive = false; };
|
||||||
|
}, [assets]);
|
||||||
|
|
||||||
|
// Canvas-rendered call text (glossy/western), keyed on the FX-relevant fields
|
||||||
|
// so dragging doesn't regenerate it. Maps element index → PNG + size.
|
||||||
|
const fxSig = t.elements.map(fxSignature).join(';');
|
||||||
|
const fxImages = useMemo(() => {
|
||||||
|
const map = new Map<number, TextFxResult>();
|
||||||
|
if (!fontsReady) return map;
|
||||||
|
t.elements.forEach((e, idx) => {
|
||||||
|
const params = buildFxParams(e);
|
||||||
|
if (!params) return;
|
||||||
|
try { map.set(idx, renderTextFx(params)); } catch { /* leave unrendered */ }
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [fxSig, fontsReady]);
|
||||||
|
|
||||||
|
// Measure the selected group for the dashed selection outline. For plain SVG
|
||||||
|
// text we use the glyphs' real ink metrics — getBBox would span the font's
|
||||||
|
// full ascent/descent. FX call images fall through to getBBox (already tight).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selected === null || selected === undefined) { setSelBox(null); return; }
|
if (selected === null || selected === undefined) { setSelBox(null); return; }
|
||||||
const key = String(selected);
|
const key = String(selected);
|
||||||
@@ -306,7 +374,8 @@ export function CardPreview({ model, assets, width, selected, onSelect, onMove,
|
|||||||
if (!g) { setSelBox(null); return; }
|
if (!g) { setSelBox(null); return; }
|
||||||
const transform = g.getAttribute('transform') ?? '';
|
const transform = g.getAttribute('transform') ?? '';
|
||||||
const el = typeof selected === 'number' ? t.elements[selected] : undefined;
|
const el = typeof selected === 'number' ? t.elements[selected] : undefined;
|
||||||
if (el && (el.type === 'callsign' || el.type === 'operator' || el.type === 'info_line') && el.size) {
|
const isFx = el ? !!fxKindFor(el.style_preset) : false;
|
||||||
|
if (!isFx && el && (el.type === 'callsign' || el.type === 'operator' || el.type === 'info_line') && el.size) {
|
||||||
const cv = (measureCanvas.current ??= document.createElement('canvas'));
|
const cv = (measureCanvas.current ??= document.createElement('canvas'));
|
||||||
const ctx = cv.getContext('2d');
|
const ctx = cv.getContext('2d');
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
@@ -325,7 +394,7 @@ export function CardPreview({ model, assets, width, selected, onSelect, onMove,
|
|||||||
}
|
}
|
||||||
const b = g.getBBox();
|
const b = g.getBBox();
|
||||||
setSelBox({ t: transform, x: b.x, y: b.y, w: b.width, h: b.height });
|
setSelBox({ t: transform, x: b.x, y: b.y, w: b.width, h: b.height });
|
||||||
}, [selected, model, assets, width, t.elements]);
|
}, [selected, model, assets, width, t.elements, fxImages]);
|
||||||
|
|
||||||
const startDrag = (sel: Exclude<CardSelection, null>, ox: number, oy: number) =>
|
const startDrag = (sel: Exclude<CardSelection, null>, ox: number, oy: number) =>
|
||||||
(ev: React.PointerEvent<SVGGElement>) => {
|
(ev: React.PointerEvent<SVGGElement>) => {
|
||||||
@@ -435,19 +504,25 @@ export function CardPreview({ model, assets, width, selected, onSelect, onMove,
|
|||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Text elements. The inner translate pulls the glyphs up by the em-box
|
// Text elements. Glossy/western calls render through the ported canvas
|
||||||
// top→cap-height gap so the element's y means "top of the visible
|
// FX as an <image> (its trimmed bounds already give a tight frame, so
|
||||||
// letters" — without it the call sits ~0.2em below its box and can't
|
// no CAP_INSET shim). Everything else is plain SVG text; the inner
|
||||||
// be pushed to the card's top edge.
|
// translate pulls the glyphs up to the em-box cap line so the element's
|
||||||
|
// y means "top of the visible letters".
|
||||||
|
const fx = fxImages.get(idx);
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
key={key} ref={setGroupRef(key)} transform={elementTransform(e)}
|
key={key} ref={setGroupRef(key)} transform={elementTransform(e)}
|
||||||
onPointerDown={startDrag(idx, e.x, e.y)}
|
onPointerDown={startDrag(idx, e.x, e.y)}
|
||||||
style={{ cursor: onMove ? 'move' : undefined }}
|
style={{ cursor: onMove ? 'move' : undefined }}
|
||||||
>
|
>
|
||||||
<g transform={`translate(0 ${-(e.size ?? 0) * CAP_INSET})`}>
|
{fx ? (
|
||||||
<TextStack e={e} idx={idx} content={e.text ?? ''} />
|
<image href={fx.url} x={0} y={0} width={fx.w} height={fx.h} />
|
||||||
</g>
|
) : (
|
||||||
|
<g transform={`translate(0 ${-(e.size ?? 0) * CAP_INSET})`}>
|
||||||
|
<TextStack e={e} idx={idx} content={e.text ?? ''} />
|
||||||
|
</g>
|
||||||
|
)}
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ export function SendEQSLModal({ open, qsoId, onClose, onOpenDesigner }: Props) {
|
|||||||
const jpeg = await rasterizeCard(svgEl.current, card.w, card.h, 'image/jpeg');
|
const jpeg = await rasterizeCard(svgEl.current, card.w, card.h, 'image/jpeg');
|
||||||
await SendEQSL(qsoId, templateId, jpeg);
|
await SendEQSL(qsoId, templateId, jpeg);
|
||||||
setSent(true);
|
setSent(true);
|
||||||
|
// Auto-close shortly after the success tick (the grid refreshes on the
|
||||||
|
// qsl:sent event); the user doesn't need to dismiss it manually.
|
||||||
|
window.setTimeout(onClose, 1100);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(String(e));
|
setError(String(e));
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Input } from '@/components/ui/input';
|
|||||||
import {
|
import {
|
||||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import type { QSLPresetInfo, StyleParams } from './qslTypes';
|
import type { QSLPresetInfo, StyleParams, FxParams } from './qslTypes';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
presets: QSLPresetInfo[];
|
presets: QSLPresetInfo[];
|
||||||
@@ -48,6 +48,13 @@ export function StylePresetPicker({ presets, preset, params, onChange }: Props)
|
|||||||
const has = (k: string) => info?.params.includes(k) ?? false;
|
const has = (k: string) => info?.params.includes(k) ?? false;
|
||||||
const set = (patch: Partial<StyleParams>) => onChange(preset, { ...params, ...patch });
|
const set = (patch: Partial<StyleParams>) => onChange(preset, { ...params, ...patch });
|
||||||
|
|
||||||
|
// gel_* presets are rendered by the canvas FX, not the SVG stack: show the FX
|
||||||
|
// knobs instead of the old shine/halo/shadow/outline rows.
|
||||||
|
const isFx = preset.startsWith('gel_');
|
||||||
|
const fxWestern = preset.includes('grunge');
|
||||||
|
const fx: FxParams = params.fx ?? {};
|
||||||
|
const setFx = (patch: Partial<FxParams>) => set({ fx: { ...fx, ...patch } });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Select
|
<Select
|
||||||
@@ -90,25 +97,25 @@ export function StylePresetPicker({ presets, preset, params, onChange }: Props)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{has('outline_color') && (
|
{!isFx && has('outline_color') && (
|
||||||
<ColorRow label="Outline" value={params.outline_color ?? '#2a3f5c'}
|
<ColorRow label="Outline" value={params.outline_color ?? '#2a3f5c'}
|
||||||
onChange={(v) => set({ outline_color: v })} />
|
onChange={(v) => set({ outline_color: v })} />
|
||||||
)}
|
)}
|
||||||
{has('outline_width') && (
|
{!isFx && has('outline_width') && (
|
||||||
<SliderRow label="Outline width" value={params.outline_width ?? 9} min={0} max={24} step={1}
|
<SliderRow label="Outline width" value={params.outline_width ?? 9} min={0} max={24} step={1}
|
||||||
onChange={(v) => set({ outline_width: v })} />
|
onChange={(v) => set({ outline_width: v })} />
|
||||||
)}
|
)}
|
||||||
{has('shine') && (
|
{!isFx && has('shine') && (
|
||||||
<SliderRow label="Gloss" value={params.shine?.coverage ?? 0.5} min={0.2} max={0.8} step={0.05}
|
<SliderRow label="Gloss" value={params.shine?.coverage ?? 0.5} min={0.2} max={0.8} step={0.05}
|
||||||
onChange={(v) => set({ shine: { coverage: v, opacity: params.shine?.opacity ?? 0.95 } })} />
|
onChange={(v) => set({ shine: { coverage: v, opacity: params.shine?.opacity ?? 0.95 } })} />
|
||||||
)}
|
)}
|
||||||
{has('halo') && (
|
{!isFx && has('halo') && (
|
||||||
<SliderRow label="Halo" value={params.halo?.opacity ?? 0.4} min={0} max={1} step={0.05}
|
<SliderRow label="Halo" value={params.halo?.opacity ?? 0.4} min={0} max={1} step={0.05}
|
||||||
onChange={(v) => set({
|
onChange={(v) => set({
|
||||||
halo: { color: params.halo?.color ?? '#cdd9e4', blur: params.halo?.blur ?? 6, opacity: v },
|
halo: { color: params.halo?.color ?? '#cdd9e4', blur: params.halo?.blur ?? 6, opacity: v },
|
||||||
})} />
|
})} />
|
||||||
)}
|
)}
|
||||||
{has('shadow') && (
|
{!isFx && has('shadow') && (
|
||||||
<SliderRow label="Shadow" value={params.shadow?.opacity ?? 0.5} min={0} max={1} step={0.05}
|
<SliderRow label="Shadow" value={params.shadow?.opacity ?? 0.5} min={0} max={1} step={0.05}
|
||||||
onChange={(v) => set({
|
onChange={(v) => set({
|
||||||
shadow: {
|
shadow: {
|
||||||
@@ -117,6 +124,35 @@ export function StylePresetPicker({ presets, preset, params, onChange }: Props)
|
|||||||
},
|
},
|
||||||
})} />
|
})} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isFx && !fxWestern && (
|
||||||
|
<>
|
||||||
|
<SliderRow label="Plump" value={fx.plump ?? 0.03} min={0} max={0.06} step={0.005}
|
||||||
|
onChange={(v) => setFx({ plump: v })} />
|
||||||
|
<SliderRow label="Gloss" value={fx.gloss ?? 0.85} min={0} max={1} step={0.05}
|
||||||
|
onChange={(v) => setFx({ gloss: v })} />
|
||||||
|
<SliderRow label="Gloss height" value={fx.gloss_h ?? 0.45} min={0.2} max={0.8} step={0.05}
|
||||||
|
onChange={(v) => setFx({ gloss_h: v })} />
|
||||||
|
<SliderRow label="Inner shadow" value={fx.inner_b ?? 0.7} min={0} max={1} step={0.05}
|
||||||
|
onChange={(v) => setFx({ inner_b: v })} />
|
||||||
|
<SliderRow label="Dark edge" value={fx.edge ?? 0.5} min={0} max={1} step={0.05}
|
||||||
|
onChange={(v) => setFx({ edge: v })} />
|
||||||
|
<SliderRow label="Silver rim" value={fx.outerw ?? 0.45} min={0} max={1} step={0.05}
|
||||||
|
onChange={(v) => setFx({ outerw: v })} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isFx && fxWestern && (
|
||||||
|
<>
|
||||||
|
<SliderRow label="3D depth" value={fx.depth ?? 12} min={0} max={40} step={1}
|
||||||
|
onChange={(v) => setFx({ depth: v })} />
|
||||||
|
<SliderRow label="Slant" value={fx.slant ?? 0.08} min={-0.4} max={0.4} step={0.02}
|
||||||
|
onChange={(v) => setFx({ slant: v })} />
|
||||||
|
<SliderRow label="Grunge" value={fx.grunge ?? 0.8} min={0} max={1} step={0.05}
|
||||||
|
onChange={(v) => setFx({ grunge: v })} />
|
||||||
|
<SliderRow label="Top light" value={fx.bevel ?? 0.45} min={0} max={1} step={0.05}
|
||||||
|
onChange={(v) => setFx({ bevel: v })} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,24 @@ export interface BevelFx {
|
|||||||
light: string;
|
light: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FxParams — per-call tuning of the canvas renderer (textFx.ts). All optional;
|
||||||
|
// omitted values fall back to the renderer's per-preset default.
|
||||||
|
export interface FxParams {
|
||||||
|
plump?: number;
|
||||||
|
edge?: number;
|
||||||
|
outerw?: number;
|
||||||
|
gloss?: number;
|
||||||
|
gloss_h?: number;
|
||||||
|
gloss_i?: number;
|
||||||
|
inner_b?: number;
|
||||||
|
depth?: number;
|
||||||
|
angle?: number;
|
||||||
|
slant?: number;
|
||||||
|
grunge?: number;
|
||||||
|
bevel?: number;
|
||||||
|
seed?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StyleParams {
|
export interface StyleParams {
|
||||||
gradient?: string[];
|
gradient?: string[];
|
||||||
shine?: Shine;
|
shine?: Shine;
|
||||||
@@ -63,6 +81,7 @@ export interface StyleParams {
|
|||||||
shadow?: ShadowFx;
|
shadow?: ShadowFx;
|
||||||
bevel_offset?: BevelFx;
|
bevel_offset?: BevelFx;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
fx?: FxParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ElementType = 'callsign' | 'operator' | 'info_line' | 'country' | 'insert';
|
export type ElementType = 'callsign' | 'operator' | 'info_line' | 'country' | 'insert';
|
||||||
|
|||||||
@@ -1738,6 +1738,42 @@ export namespace qslcard {
|
|||||||
this.light = source["light"];
|
this.light = source["light"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export class FxParams {
|
||||||
|
plump?: number;
|
||||||
|
edge?: number;
|
||||||
|
outerw?: number;
|
||||||
|
gloss?: number;
|
||||||
|
gloss_h?: number;
|
||||||
|
gloss_i?: number;
|
||||||
|
inner_b?: number;
|
||||||
|
depth?: number;
|
||||||
|
angle?: number;
|
||||||
|
slant?: number;
|
||||||
|
grunge?: number;
|
||||||
|
bevel?: number;
|
||||||
|
seed?: number;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new FxParams(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.plump = source["plump"];
|
||||||
|
this.edge = source["edge"];
|
||||||
|
this.outerw = source["outerw"];
|
||||||
|
this.gloss = source["gloss"];
|
||||||
|
this.gloss_h = source["gloss_h"];
|
||||||
|
this.gloss_i = source["gloss_i"];
|
||||||
|
this.inner_b = source["inner_b"];
|
||||||
|
this.depth = source["depth"];
|
||||||
|
this.angle = source["angle"];
|
||||||
|
this.slant = source["slant"];
|
||||||
|
this.grunge = source["grunge"];
|
||||||
|
this.bevel = source["bevel"];
|
||||||
|
this.seed = source["seed"];
|
||||||
|
}
|
||||||
|
}
|
||||||
export class Halo {
|
export class Halo {
|
||||||
color: string;
|
color: string;
|
||||||
blur: number;
|
blur: number;
|
||||||
@@ -1797,6 +1833,7 @@ export namespace qslcard {
|
|||||||
shadow?: ShadowFx;
|
shadow?: ShadowFx;
|
||||||
bevel_offset?: Bevel;
|
bevel_offset?: Bevel;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
fx?: FxParams;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new StyleParams(source);
|
return new StyleParams(source);
|
||||||
@@ -1812,6 +1849,7 @@ export namespace qslcard {
|
|||||||
this.shadow = this.convertValues(source["shadow"], ShadowFx);
|
this.shadow = this.convertValues(source["shadow"], ShadowFx);
|
||||||
this.bevel_offset = this.convertValues(source["bevel_offset"], Bevel);
|
this.bevel_offset = this.convertValues(source["bevel_offset"], Bevel);
|
||||||
this.color = source["color"];
|
this.color = source["color"];
|
||||||
|
this.fx = this.convertValues(source["fx"], FxParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type Preset struct {
|
|||||||
// gelParams are the knobs shared by the layered "gel" stacks.
|
// gelParams are the knobs shared by the layered "gel" stacks.
|
||||||
var gelParams = map[string]bool{
|
var gelParams = map[string]bool{
|
||||||
"gradient": true, "shine": true, "outline_color": true, "outline_width": true,
|
"gradient": true, "shine": true, "outline_color": true, "outline_width": true,
|
||||||
"halo": true, "shadow": true, "bevel_offset": true,
|
"halo": true, "shadow": true, "bevel_offset": true, "fx": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Presets is the built-in style registry, keyed by preset name.
|
// Presets is the built-in style registry, keyed by preset name.
|
||||||
@@ -23,8 +23,8 @@ var Presets = map[string]Preset{
|
|||||||
Label: "Gel gold",
|
Label: "Gel gold",
|
||||||
AllowedParams: gelParams,
|
AllowedParams: gelParams,
|
||||||
Defaults: StyleParams{
|
Defaults: StyleParams{
|
||||||
Gradient: []string{"#FFE15A", "#FFC312", "#E07A00"},
|
Gradient: []string{"#FFE22D", "#FFD600", "#FFCC00"}, // bright XV9Q yellows; orange depth comes from the FX inner shadow
|
||||||
Shine: &Shine{Coverage: 0.52, Opacity: 0.95},
|
Shine: &Shine{Coverage: 0.6, Opacity: 1},
|
||||||
OutlineColor: "#2a3f5c", OutlineWidth: 10,
|
OutlineColor: "#2a3f5c", OutlineWidth: 10,
|
||||||
Halo: &Halo{Color: "#cdd9e4", Blur: 6, Opacity: 0.4},
|
Halo: &Halo{Color: "#cdd9e4", Blur: 6, Opacity: 0.4},
|
||||||
Shadow: &ShadowFx{Dx: 6, Dy: 9, Blur: 5, Color: "#14243a", Opacity: 0.55},
|
Shadow: &ShadowFx{Dx: 6, Dy: 9, Blur: 5, Color: "#14243a", Opacity: 0.55},
|
||||||
|
|||||||
@@ -85,6 +85,27 @@ type StyleParams struct {
|
|||||||
Shadow *ShadowFx `json:"shadow,omitempty"`
|
Shadow *ShadowFx `json:"shadow,omitempty"`
|
||||||
BevelOffset *Bevel `json:"bevel_offset,omitempty"`
|
BevelOffset *Bevel `json:"bevel_offset,omitempty"`
|
||||||
Color string `json:"color,omitempty"`
|
Color string `json:"color,omitempty"`
|
||||||
|
Fx *FxParams `json:"fx,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FxParams tunes the canvas call renderer (glossy bubble / western 3D). All
|
||||||
|
// optional pointers — nil falls back to the renderer's per-preset default.
|
||||||
|
// Persisted in the template so per-call tweaks survive a round-trip; the values
|
||||||
|
// are consumed entirely by the frontend (textFx.ts).
|
||||||
|
type FxParams struct {
|
||||||
|
Plump *float64 `json:"plump,omitempty"`
|
||||||
|
Edge *float64 `json:"edge,omitempty"`
|
||||||
|
OuterW *float64 `json:"outerw,omitempty"`
|
||||||
|
Gloss *float64 `json:"gloss,omitempty"`
|
||||||
|
GlossH *float64 `json:"gloss_h,omitempty"`
|
||||||
|
GlossI *float64 `json:"gloss_i,omitempty"`
|
||||||
|
InnerB *float64 `json:"inner_b,omitempty"`
|
||||||
|
Depth *float64 `json:"depth,omitempty"`
|
||||||
|
Angle *float64 `json:"angle,omitempty"`
|
||||||
|
Slant *float64 `json:"slant,omitempty"`
|
||||||
|
Grunge *float64 `json:"grunge,omitempty"`
|
||||||
|
Bevel *float64 `json:"bevel,omitempty"`
|
||||||
|
Seed *float64 `json:"seed,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// setKeys lists the JSON names of the params that are actually set, for
|
// setKeys lists the JSON names of the params that are actually set, for
|
||||||
@@ -118,6 +139,9 @@ func (p *StyleParams) setKeys() []string {
|
|||||||
if p.Color != "" {
|
if p.Color != "" {
|
||||||
keys = append(keys, "color")
|
keys = append(keys, "color")
|
||||||
}
|
}
|
||||||
|
if p.Fx != nil {
|
||||||
|
keys = append(keys, "fx")
|
||||||
|
}
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user