Vector shapes can fill with gradients and textures (#186)
A vector shape could only be filled and outlined with one flat colour. Shading, glows and patterned surfaces had to be faked with stacks of shapes. Every shape now has a paint type for its fill and its outline. - Fill type: solid colour, linear gradient, radial gradient or texture. Line type offers the same without texture. - Colour stops are edited on a bar in the inspector, as agreed in the issue. A click on the bar adds a stop in the colour the gradient already has there, so adding never changes the picture. At least two stops remain. - The new Paint tool in the tab shows two handles for the selected shape: axis start and end, centre and radius, or the corners of one texture tile. "Handles: Fill / Outline" picks which paint is edited. - Moving a whole shape moves its gradients and tile with it. Every gesture is one undo step. - A texture fill always tiles, because Pixi turns a non-repeating pattern into a repeating one anyway. When switched on, one tile covers the whole shape. - Only uploaded textures and frames can be a fill, never a drawing. A drawing filled with itself would rasterise itself mid-raster. - The fingerprint of a drawing includes the texture and frame a fill points to, so the picture redraws when the image is swapped. Findings: - graphics.destroy(true) also destroyed the textures of the fills. With a texture fill it deleted the tile slice and the next redraw crashed on reading 'scaleMode' of null. It is now destroy(). - The gradient cache destroyed old gradients itself, which gave 20 Pixi warnings "destroyed while still bound". They are now handed to Pixi's texture GC with autoGarbageCollect switched on. The console of module 47 is empty again, the last CI log had 0 of them. Tests: - ./check.sh: 135 files, 2011 tests, exit 0 (before 134 / 1986) - npm run build: exit 0 - GM_STEPS=47, freshly seeded: ALL PASSED, 54 checks (before 44), 0 console warnings - 25 new unit tests, 10 new e2e checks in block 47i - Four counter-checks bite, originals restored and verified with cmp - Check sum 1232 in README and wiki, calculated (1222 + 10) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1116818011
commit
592ea942d9
22 files changed
+2090
-98
No files matched your search
@@ -9,10 +9,11 @@
|
||||
* `model/vectorart.ts`), so a command that edits a shape cannot forget to say that it did.
|
||||
*/
|
||||
import { newId } from '../ids';
|
||||
import type { VectorRing, VectorShape, VectorStroke } from '../model/Asset';
|
||||
import type { VectorPaint, VectorRing, VectorShape, VectorStroke } from '../model/Asset';
|
||||
import type { Project } from '../model/Project';
|
||||
import { cloneVectorRing, SHAPE_FILL_DEFAULT } from '../model/vectorart';
|
||||
import { combineBlocker, combinedRings, type ShapeBoolean } from '../model/vectorBoolean';
|
||||
import { cloneGradient, clonePaint, MIN_STOPS } from '../model/vectorPaint';
|
||||
import { moveItem, type Removed, removeById, removeFirst, removeWhere, restoreAt } from '../util/list';
|
||||
import { type Command, CommandError } from './Command';
|
||||
import { nextNumberedName } from './naming';
|
||||
@@ -200,20 +201,29 @@ export class MoveVectorShapeCommand implements Command {
|
||||
}
|
||||
}
|
||||
|
||||
/** Where the fill and the outline of a shape are painted from – what moves when the shape is moved. */
|
||||
export interface ShapePaints {
|
||||
paint?: VectorPaint;
|
||||
strokePaint?: VectorStroke['paint'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the outlines of one shape – a point dragged, a point inserted on an edge, a handle pulled,
|
||||
* a ring closed. The rings are copied in and out, so the editor may hand over the list it was
|
||||
* dragging on without the undo step pointing at live data.
|
||||
* dragging on without the undo step pointing at live data. A whole shape dragged hands over its
|
||||
* paints too (#186), so a gradient travels with the shape instead of staying where it was.
|
||||
*/
|
||||
export class SetVectorShapeRingsCommand implements Command {
|
||||
readonly label: string;
|
||||
private previous: VectorRing[] = [];
|
||||
private previousPaints: ShapePaints = {};
|
||||
|
||||
constructor(
|
||||
private readonly assetId: string,
|
||||
private readonly shapeId: string,
|
||||
private readonly rings: readonly VectorRing[],
|
||||
label = 'Edit shape',
|
||||
private readonly paints: ShapePaints | null = null,
|
||||
) {
|
||||
this.label = label;
|
||||
}
|
||||
@@ -229,17 +239,42 @@ export class SetVectorShapeRingsCommand implements Command {
|
||||
const shape = requireVectorShape(project, this.assetId, this.shapeId);
|
||||
this.previous = shape.rings;
|
||||
shape.rings = cloneRings(this.rings);
|
||||
if (this.paints) {
|
||||
this.previousPaints = paintsOf(shape);
|
||||
applyPaints(shape, this.paints);
|
||||
}
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
requireVectorShape(project, this.assetId, this.shapeId).rings = this.previous;
|
||||
const shape = requireVectorShape(project, this.assetId, this.shapeId);
|
||||
shape.rings = this.previous;
|
||||
if (this.paints) applyPaints(shape, this.previousPaints);
|
||||
}
|
||||
}
|
||||
|
||||
/** What `SetVectorShapeStyleCommand` writes; a field left out stays as it is, `stroke: null` removes it. */
|
||||
export function paintsOf(shape: VectorShape): ShapePaints {
|
||||
return {
|
||||
...(shape.paint ? { paint: clonePaint(shape.paint) } : {}),
|
||||
...(shape.stroke?.paint ? { strokePaint: cloneGradient(shape.stroke.paint) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function applyPaints(shape: VectorShape, paints: ShapePaints): void {
|
||||
if (paints.paint) shape.paint = clonePaint(paints.paint);
|
||||
else delete shape.paint;
|
||||
if (!shape.stroke) return;
|
||||
if (paints.strokePaint) shape.stroke.paint = cloneGradient(paints.strokePaint);
|
||||
else delete shape.stroke.paint;
|
||||
}
|
||||
|
||||
/**
|
||||
* What `SetVectorShapeStyleCommand` writes; a field left out stays as it is, `null` removes it. A
|
||||
* stroke brings its own paint along, as a whole.
|
||||
*/
|
||||
export interface ShapeStyle {
|
||||
fill?: number;
|
||||
opacity?: number;
|
||||
paint?: VectorPaint | null;
|
||||
stroke?: VectorStroke | null;
|
||||
}
|
||||
|
||||
@@ -263,7 +298,15 @@ export class SetVectorShapeStyleCommand implements Command {
|
||||
throw new CommandError('An opacity is a number');
|
||||
if (this.style.stroke && !(this.style.stroke.width > 0))
|
||||
throw new CommandError('An outline is wider than nothing');
|
||||
this.previous = { fill: shape.fill, opacity: shape.opacity, stroke: shape.stroke ?? null };
|
||||
for (const paint of [this.style.paint, this.style.stroke?.paint])
|
||||
if (paint && paint.type !== 'texture' && paint.stops.length < MIN_STOPS)
|
||||
throw new CommandError('A gradient needs two colours');
|
||||
this.previous = {
|
||||
fill: shape.fill,
|
||||
opacity: shape.opacity,
|
||||
paint: shape.paint ? clonePaint(shape.paint) : null,
|
||||
stroke: shape.stroke ? cloneStroke(shape.stroke) : null,
|
||||
};
|
||||
applyStyle(shape, this.style);
|
||||
}
|
||||
|
||||
@@ -275,12 +318,24 @@ export class SetVectorShapeStyleCommand implements Command {
|
||||
function applyStyle(shape: VectorShape, style: ShapeStyle): void {
|
||||
if (style.fill !== undefined) shape.fill = style.fill;
|
||||
if (style.opacity !== undefined) shape.opacity = Math.max(0, Math.min(1, style.opacity));
|
||||
if (style.paint !== undefined) {
|
||||
if (style.paint) shape.paint = clonePaint(style.paint);
|
||||
else delete shape.paint;
|
||||
}
|
||||
if (style.stroke !== undefined) {
|
||||
if (style.stroke) shape.stroke = { ...style.stroke };
|
||||
if (style.stroke) shape.stroke = cloneStroke(style.stroke);
|
||||
else delete shape.stroke;
|
||||
}
|
||||
}
|
||||
|
||||
function cloneStroke(stroke: VectorStroke): VectorStroke {
|
||||
return {
|
||||
color: stroke.color,
|
||||
width: stroke.width,
|
||||
...(stroke.paint ? { paint: cloneGradient(stroke.paint) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function cloneRings(rings: readonly VectorRing[]): VectorRing[] {
|
||||
return rings.map(cloneVectorRing);
|
||||
}
|
||||
Reference in new issue
Block a user