Draw with tools: a pen, a brush that makes polygons, and knockouts
One tool at a time, picked from a strip down the left of the stage or by its letter, as in Photoshop, Illustrator and Flash: V selects and transforms, P is the pen, B the brush, E the eraser. The options bar above the stage holds the tool's settings, and the palette is a grid under the tools with the colour a new shape gets above it, as Deluxe Paint's was. The pen's draft is filled into the picture as it is drawn, so the edge on screen is the pixels the shape will be. Click the first point, Enter, Esc or leaving the pen closes it; the pen stays the tool. Editing a shape's points is the pen ON that shape — Figma's vector edit mode — so the separate points mode is gone: click an edge to add a point, ⌥-click one to delete it, on every key at once so tweens keep meaning something. The brush paints a mask, and what it will be is shown while painting: the stroke is traced round its pixel edges, holes and all, and simplified to a point every so many pixels of outline by the same function the saved shapes come from. A hole is bridged into the one ring along a whole-pixel row, which the fill never samples. Blender's Adjust Last Operation re-traces the last stroke from what it was made of. A shape coloured CLEAR is a knockout: its symbol is drawn into a layer of its own and the knockout clears it, every colour or one. The eraser makes one in the symbol it starts on, of the colour it starts on, or of every colour with ⌥. A click goes through a knockout to what shows. Previews are drawn in the stacking context of what they will land in. The polygon's points no longer stay behind when the shape is moved: they were read off the saved document while the box handles read the one mid-drag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
551d572347
commit
1f0b4d9918
21 changed files with 1479 additions and 277 deletions
|
|
@ -778,7 +778,7 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
|||
.tab:hover .close, .tab.on .close { visibility: visible; }
|
||||
.tab .close:hover { background: var(--hair); color: var(--fg); }
|
||||
|
||||
.palette-bar {
|
||||
.options-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
|
|
@ -876,8 +876,6 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
|||
.paint-overlay { position: absolute; inset: 0; touch-action: none; }
|
||||
/* The footage traced over: a reference above the picture, never part of it. */
|
||||
.underlay { position: absolute; inset: 0; pointer-events: none; }
|
||||
.paint-overlay.drawing { cursor: crosshair; }
|
||||
.paint-overlay circle { cursor: grab; }
|
||||
|
||||
/* Where a drag out of the pool would land. Dashed and unfilled, so it reads as
|
||||
"not there yet" over whatever is already drawn. */
|
||||
|
|
@ -1526,8 +1524,8 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
|||
.stage-area { padding: 5px; }
|
||||
|
||||
/* Touch targets, in the strips that are all buttons. */
|
||||
.pane-head, .palette-bar { gap: 7px; }
|
||||
.palette-bar button, .pane-head button, .top button { min-height: 24px; }
|
||||
.pane-head, .options-bar { gap: 7px; }
|
||||
.options-bar button, .pane-head button, .top button { min-height: 24px; }
|
||||
|
||||
/* A STRIP OF CONTROLS SCROLLS AS A STRIP. Flexbox's instinct when a row does
|
||||
not fit is to shrink every item and then push the last ones off the end —
|
||||
|
|
@ -1536,17 +1534,138 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
|||
So nothing shrinks, nothing wraps, and each strip scrolls. The `.spacer`
|
||||
and `.status` rules above still win on specificity, which is what keeps the
|
||||
right-hand groups right-hand while there IS room. */
|
||||
.top > *, .pane-head > *, .palette-bar > * { flex-shrink: 0; }
|
||||
.top > *, .pane-head > *, .options-bar > * { flex-shrink: 0; }
|
||||
.top button, .pane-head button { white-space: nowrap; }
|
||||
.pane-head, .palette-bar { overflow-x: auto; }
|
||||
|
||||
/* The one compressible thing in the palette bar is the swatches, so they
|
||||
scroll inside their own share instead of taking it from the commands. The
|
||||
tone's NAME goes: the ringed swatch already says which one is active, and
|
||||
it is the widest thing in the bar that nothing is lost by dropping. */
|
||||
.palette-bar .swatches { flex: 1 1 130px; min-width: 56px; overflow-x: auto; }
|
||||
/* A swatch is a 15px circle or it is not a swatch: the strip scrolls, the
|
||||
dots do not get thinner. */
|
||||
.palette-bar .swatches > * { flex-shrink: 0; }
|
||||
.palette-bar > .dim { display: none; }
|
||||
.pane-head, .options-bar { overflow-x: auto; }
|
||||
/* The hints go first: the tool's name and its controls are what is needed. */
|
||||
.options-bar .hint { display: none; }
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
the tools: a strip down the left of the stage, an options bar above it.
|
||||
|
||||
Photoshop's arrangement, which Illustrator and Flash share. The palette is a
|
||||
grid in the strip under the tools, as Deluxe Paint's and Animator Pro's were:
|
||||
a fixed table of slots, shown as one. */
|
||||
|
||||
.options-bar {
|
||||
gap: 8px;
|
||||
min-height: 29px;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
.options-bar .tool-name { font-weight: 600; min-width: 46px; }
|
||||
.options-bar .hint { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 0 1 auto; }
|
||||
.options-bar .spacer { flex: 1; }
|
||||
.option { display: flex; align-items: center; gap: 6px; color: var(--dim); white-space: nowrap; }
|
||||
.option input[type="range"] { width: 110px; }
|
||||
.option .value { color: var(--fg); min-width: 30px; font-variant-numeric: tabular-nums; }
|
||||
.palette-assets { display: flex; align-items: center; gap: 3px; }
|
||||
|
||||
.workspace { flex: 1; min-height: 0; display: flex; }
|
||||
.workspace > .stage-area { flex: 1; min-width: 0; position: relative; }
|
||||
|
||||
.toolbox {
|
||||
flex: 0 0 52px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 7px 0;
|
||||
background: var(--chrome);
|
||||
border-right: 1px solid var(--line);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tools { display: grid; gap: 2px; }
|
||||
.tool {
|
||||
width: 34px; height: 30px;
|
||||
display: grid; place-items: center;
|
||||
padding: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: none;
|
||||
color: var(--fg);
|
||||
}
|
||||
.tool:hover { background: var(--sunk); border-color: var(--hair); }
|
||||
.tool.on { background: var(--sel-bg); border-color: var(--sel); color: var(--sel); }
|
||||
.tool svg { fill: currentColor; stroke: none; }
|
||||
.tool svg .cut { fill: none; stroke: var(--chrome); stroke-width: 1.2; }
|
||||
.tool.on svg .cut { stroke: var(--sel-bg); }
|
||||
|
||||
.palette { display: grid; gap: 7px; justify-items: center; padding-top: 9px; border-top: 1px solid var(--hair); }
|
||||
|
||||
/* The colour a new shape gets: Photoshop's foreground chip. */
|
||||
.palette .chip {
|
||||
width: 36px; height: 36px;
|
||||
display: grid; place-items: end start;
|
||||
border: 1px solid var(--fg);
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 0 0 2px var(--pane);
|
||||
}
|
||||
.palette .chip span {
|
||||
margin: 0 0 2px 3px; padding: 0 3px;
|
||||
border-radius: 2px;
|
||||
background: rgba(255, 255, 255, .85);
|
||||
color: var(--fg); font-size: 9px; line-height: 12px;
|
||||
}
|
||||
|
||||
.palette .cells { display: grid; grid-template-columns: repeat(2, 17px); gap: 2px; }
|
||||
.palette .slot { display: contents; }
|
||||
.palette .cell {
|
||||
width: 17px; height: 17px;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(0, 0, 0, .35);
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.palette .cell:hover { border-color: var(--fg); transform: scale(1.12); }
|
||||
.palette .cell.on { border-color: var(--fg); box-shadow: 0 0 0 1px var(--pane), 0 0 0 2px var(--sel); position: relative; z-index: 1; }
|
||||
|
||||
/* Clear is no colour: a knockout. Checkered, as transparency is everywhere. */
|
||||
.palette .clear,
|
||||
.palette .chip.clear {
|
||||
background-color: #fff;
|
||||
background-image:
|
||||
linear-gradient(45deg, #c9c9c5 25%, transparent 25%, transparent 75%, #c9c9c5 75%),
|
||||
linear-gradient(45deg, #c9c9c5 25%, transparent 25%, transparent 75%, #c9c9c5 75%);
|
||||
background-size: 8px 8px;
|
||||
background-position: 0 0, 4px 4px;
|
||||
}
|
||||
.palette .cell.clear { grid-column: span 2; width: 36px; }
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
on the stage: the tools' marks, in stage pixels (the SVG's viewBox), and their
|
||||
cursors. Hairlines, so the pixels under them can be judged. */
|
||||
|
||||
.paint-overlay.tool-pen {
|
||||
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22'%3E%3Cpath d='M2 2 L9 5 L14 11 L11 14 L5 9 Z' fill='%23111' stroke='%23fff' stroke-width='1.2' stroke-linejoin='round'/%3E%3Cpath d='M2 2 L7 7' stroke='%23fff' stroke-width='1'/%3E%3C/svg%3E") 2 2, crosshair;
|
||||
}
|
||||
.paint-overlay.tool-pen.closing {
|
||||
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22'%3E%3Cpath d='M2 2 L9 5 L14 11 L11 14 L5 9 Z' fill='%23111' stroke='%23fff' stroke-width='1.2' stroke-linejoin='round'/%3E%3Ccircle cx='17' cy='17' r='3' fill='none' stroke='%23111' stroke-width='1.6'/%3E%3Ccircle cx='17' cy='17' r='3' fill='none' stroke='%23fff' stroke-width='.6'/%3E%3C/svg%3E") 2 2, crosshair;
|
||||
}
|
||||
.paint-overlay.tool-brush, .paint-overlay.tool-eraser { cursor: none; }
|
||||
|
||||
.paint-overlay .footprint { fill: none; stroke: #fff1be; stroke-width: 0.5; vector-effect: non-scaling-stroke; pointer-events: none; }
|
||||
.paint-overlay .footprint.eraser { stroke-dasharray: 3 2; }
|
||||
|
||||
.paint-overlay .draft polyline { fill: none; stroke: #fff1be; stroke-width: 1; vector-effect: non-scaling-stroke; pointer-events: none; }
|
||||
.paint-overlay .draft .first { fill: #161820; stroke: #fff1be; stroke-width: 0.5; pointer-events: none; }
|
||||
.paint-overlay .draft .first.closing { fill: #fff1be; r: 2.6; }
|
||||
|
||||
.paint-overlay .points .outline { fill: none; stroke: #fff1be; stroke-width: 1; stroke-opacity: .55; vector-effect: non-scaling-stroke; pointer-events: none; }
|
||||
.paint-overlay .points .vertex { fill: #fff1be; stroke: #161820; stroke-width: 0.5; cursor: move; }
|
||||
.paint-overlay .points .vertex:hover { fill: #fff; r: 2.4; }
|
||||
.paint-overlay .points .insert { fill: #161820; stroke: #fff1be; stroke-width: 0.5; pointer-events: none; }
|
||||
|
||||
/* Blender's Adjust Last Operation: the last stroke's settings, in the corner of
|
||||
the view it was made in, until something else is done. */
|
||||
.adjust-last {
|
||||
position: absolute;
|
||||
left: 10px; bottom: 10px;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 5px 9px;
|
||||
background: var(--pane);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, .25);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue