update
This commit is contained in:
parent
8ee6a87d44
commit
06176fb2d5
6 changed files with 3282 additions and 153 deletions
17
README.md
17
README.md
|
|
@ -281,3 +281,20 @@ Use this to compare minified JSON, gzip, zlib, and Brotli when available. Produc
|
|||
## v16b hotfix
|
||||
- Fixed the dark/blank world regression caused by using `source-in` compositing directly on the main world canvas for ship water reflection.
|
||||
- Ship reflections are now precomposited on an offscreen canvas before drawing, so terrain and sprites are not erased or darkened.
|
||||
|
||||
|
||||
## Phase 6 create-layout / authority prep
|
||||
|
||||
- The Create tab editor now follows the tighter canvas-first layout: top metadata row, canvas with a vertical palette, large tool buttons, and a full-width Advanced settings panel.
|
||||
- The palette ramps were rebuilt into cleaner 4-color rows so hue/lightness progression is easier to read.
|
||||
- Local browser data uses a new storage key so older palette-corrupted art is discarded automatically.
|
||||
- Shared-world prep now treats publish, object move, day/night time, and spontaneous dynamic motion as server-authoritative concerns.
|
||||
- The client can keep pending shared publish/move visuals locally while waiting for server validation, and dynamic runtime motion can interpolate toward server-supplied target positions.
|
||||
|
||||
## Phase 6b server-authority / Create tab correction
|
||||
|
||||
- CREATE layout now groups the canvas and primary tools in the left column with the palette as the right column, matching the attached design more closely.
|
||||
- Save schema was bumped to 15. Existing local art data from older palette mappings is intentionally reset and replaced with new seed artwork.
|
||||
- Added server-side `world.phase` and `dynamic.move` event helpers/tests. Shared clients should treat these events as the source of truth for day/night and autonomous dynamic positions.
|
||||
- Pending shared object moves now render as temporary local interpolation only; the persistent world state is updated only by server-issued events.
|
||||
- Depth shading can now use a cursor-local light source on rendered sprites at night.
|
||||
|
|
|
|||
96
index.html
96
index.html
|
|
@ -89,59 +89,63 @@
|
|||
</div>
|
||||
|
||||
<div class="paintLayout">
|
||||
<canvas id="paintCanvas" width="512" height="512" aria-label="Pixel editor"></canvas>
|
||||
<div class="paintMainColumn">
|
||||
<canvas id="paintCanvas" width="512" height="512" aria-label="Pixel editor"></canvas>
|
||||
<div class="toolRow editorToolRow basicEditorTools">
|
||||
<button id="toolBrush" class="tool toolBrush active" title="B">Draw</button>
|
||||
<button id="toolErase" class="tool toolErase" title="E">Erase</button>
|
||||
<button id="toolFill" class="tool toolFill" title="F">Fill</button>
|
||||
<button id="toolPick" class="tool toolPick" title="I">Pick</button>
|
||||
</div>
|
||||
<div class="toolRow editorUtilityRow">
|
||||
<button id="toggleAdvanced" class="tool toolAdvanced" type="button">+ Advanced</button>
|
||||
<button id="undoPaint" class="tool toolUndo" type="button" disabled>↶ Undo</button>
|
||||
<button id="redoPaint" class="tool toolRedo" type="button" disabled>↷ Redo</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="palettePanel">
|
||||
<div id="paletteGrid" class="paletteGrid" aria-label="Color palette"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolRow editorToolRow">
|
||||
<button id="toolBrush" class="tool toolBrush active" title="B">✎ Draw</button>
|
||||
<button id="toolErase" class="tool toolErase" title="E">⌫ Erase</button>
|
||||
<button id="toolFill" class="tool toolFill" title="F">▣ Fill</button>
|
||||
<button id="toolPick" class="tool toolPick" title="I">◆ Pick</button>
|
||||
</div>
|
||||
<div class="toolRow editorHistoryRow">
|
||||
<button id="undoPaint" class="tool toolUndo" type="button" disabled>↶ Undo</button>
|
||||
<button id="redoPaint" class="tool toolRedo" type="button" disabled>↷ Redo</button>
|
||||
</div>
|
||||
<div id="advancedToolGroup" class="advancedToolGroup" hidden>
|
||||
<div class="toolRow advancedDrawRow">
|
||||
<button id="toolLine" class="tool toolLine advancedTool" title="L">/ Line</button>
|
||||
<button id="toolRect" class="tool toolRect advancedTool" title="R">□ Rect</button>
|
||||
<button id="toolSelect" class="tool toolSelect advancedTool" title="S">▥ Select</button>
|
||||
<button id="toolDoor" class="tool toolDoor advancedTool buildingOnly">▤ Door</button>
|
||||
<button id="clearPaint" class="tool danger advancedTool">× Clear</button>
|
||||
<div class="advancedPanel">
|
||||
<div class="advancedPanelHeader">Advanced settings</div>
|
||||
<div id="advancedToolGroup" class="advancedToolGroup" hidden>
|
||||
<div class="toolRow advancedDrawRow">
|
||||
<button id="toolLine" class="tool toolLine advancedTool" title="L">Line</button>
|
||||
<button id="toolRect" class="tool toolRect advancedTool" title="R">Rect</button>
|
||||
<button id="toolSelect" class="tool toolSelect advancedTool" title="S">Select</button>
|
||||
<button id="toolDoor" class="tool toolDoor advancedTool buildingOnly">Door</button>
|
||||
<button id="clearPaint" class="tool danger advancedTool">Clear</button>
|
||||
</div>
|
||||
<div class="toolRow advancedActionRow">
|
||||
<button id="outlinePaint" class="tool toolOutline advancedTool" type="button">Outline</button>
|
||||
<button id="flipHorizontal" class="tool toolFlipH advancedTool" type="button">Flip H</button>
|
||||
<button id="flipVertical" class="tool toolFlipV advancedTool" type="button">Flip V</button>
|
||||
<button id="clearSelection" class="tool toolClearSel advancedTool" type="button" disabled>Clear Sel</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolRow advancedActionRow">
|
||||
<button id="outlinePaint" class="tool toolOutline advancedTool" type="button">▧ Outline</button>
|
||||
<button id="flipHorizontal" class="tool toolFlipH advancedTool" type="button">⇄ Flip H</button>
|
||||
<button id="flipVertical" class="tool toolFlipV advancedTool" type="button">⇅ Flip V</button>
|
||||
<button id="clearSelection" class="tool toolClearSel advancedTool" type="button" disabled>◇ Clear Sel</button>
|
||||
<div class="advancedRow extraAdvancedRow">
|
||||
<button id="toolLight" class="tool toolLight advancedOnly" hidden>Light</button>
|
||||
<button id="toolParticle" class="tool toolParticle advancedOnly" hidden>Particle</button>
|
||||
<button id="toolDepth" class="tool toolDepth advancedOnly" hidden>Depth</button>
|
||||
<button id="depthHigh" class="tool advancedOnly" type="button" hidden>High</button>
|
||||
<button id="depthLow" class="tool advancedOnly" type="button" hidden>Low</button>
|
||||
<div id="particleDirectionWrap" class="particleControl particleControlGroup advancedOnly" hidden>
|
||||
<label>Direction
|
||||
<select id="particleDirection">
|
||||
<option value="up" selected>↑ Up</option>
|
||||
<option value="down">↓ Down</option>
|
||||
<option value="left">← Left</option>
|
||||
<option value="right">→ Right</option>
|
||||
</select>
|
||||
</label>
|
||||
<small id="particleRangeStatus">Particle: paint cells like Light/Depth. Shift/right-click clears a cell.</small>
|
||||
</div>
|
||||
</div>
|
||||
<span id="advancedHint" class="hint" hidden>Advanced reveals line, rectangle, selection, flip, light, depth, and particles.</span>
|
||||
<input id="paintColor" type="color" value="#6bd06b" hidden />
|
||||
<div id="editHint" class="hint">Palette color is used for pixels, lights, particles, and depth marks. Shortcuts: B/E/F/I/L/R/S, Ctrl/Cmd+Z, arrows move a selection.</div>
|
||||
</div>
|
||||
<div class="advancedRow">
|
||||
<button id="toggleAdvanced" class="tool toolAdvanced" type="button">+ Advanced</button>
|
||||
<button id="toolLight" class="tool toolLight advancedOnly" hidden>✹ Light</button>
|
||||
<button id="toolParticle" class="tool toolParticle advancedOnly" hidden>⁕ Particle</button>
|
||||
<button id="toolDepth" class="tool toolDepth advancedOnly" hidden>▨ Depth</button>
|
||||
<button id="depthHigh" class="tool advancedOnly" type="button" hidden>▲ High</button>
|
||||
<button id="depthLow" class="tool advancedOnly" type="button" hidden>▼ Low</button>
|
||||
<div id="particleDirectionWrap" class="particleControl particleControlGroup advancedOnly" hidden>
|
||||
<label>Direction
|
||||
<select id="particleDirection">
|
||||
<option value="up" selected>↑ Up</option>
|
||||
<option value="down">↓ Down</option>
|
||||
<option value="left">← Left</option>
|
||||
<option value="right">→ Right</option>
|
||||
</select>
|
||||
</label>
|
||||
<small id="particleRangeStatus">Particle: paint cells like Light/Depth. Shift/right-click clears a cell.</small>
|
||||
</div>
|
||||
<span id="advancedHint" class="hint" hidden>Advanced tools stay below the basic draw row. Particle paints emitter cells with palette color and direction. Shift/right-click clears a particle cell.</span>
|
||||
</div>
|
||||
<input id="paintColor" type="color" value="#6bd06b" hidden />
|
||||
<div id="editHint" class="hint">Palette color is used for pixels, lights, particles, and depth marks. Shortcuts: B/E/F/I/L/R/S, Ctrl/Cmd+Z, arrows move a selection.</div>
|
||||
</div>
|
||||
|
||||
<div class="card stack summonCard">
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ from world_policy import (
|
|||
can_delete_asset,
|
||||
can_import_full_state,
|
||||
can_modify_object,
|
||||
make_dynamic_move_event,
|
||||
make_world_phase_event,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -117,6 +119,27 @@ class WorldPolicyTest(unittest.TestCase):
|
|||
self.assertIn("obj-a", state["tombstones"]["objects"])
|
||||
self.assertEqual(len(state["dynamicSummons"]), 1)
|
||||
|
||||
|
||||
def test_world_phase_event_updates_authoritative_clock(self):
|
||||
state = base_state()
|
||||
event = make_world_phase_event(at=600_000)
|
||||
self.assertTrue(apply_server_event(state, event))
|
||||
self.assertEqual(state["serverSync"]["clock"]["worldTimeMs"], 600_000)
|
||||
self.assertEqual(state["serverSync"]["clock"]["dayMs"], 600_000)
|
||||
self.assertEqual(state["serverSync"]["lastServerEventId"], event["serverEventId"])
|
||||
|
||||
def test_dynamic_move_event_is_server_authoritative(self):
|
||||
state = base_state()
|
||||
event = make_dynamic_move_event(state, "dyn-b", x=4.5, y=7.0, target_x=8.0, target_y=9.0, at=700, actor="server")
|
||||
self.assertIsNotNone(event)
|
||||
self.assertEqual(event["type"], "dynamic.move")
|
||||
self.assertTrue(apply_server_event(state, event))
|
||||
target = state["serverSync"]["dynamicTargets"]["dyn-b"]
|
||||
self.assertEqual(target["targetX"], 8.0)
|
||||
self.assertEqual(target["targetY"], 9.0)
|
||||
self.assertEqual(state["dynamicSummons"][0]["serverState"]["x"], 4.5)
|
||||
self.assertEqual(state["dynamicSummons"][0]["version"], 2)
|
||||
|
||||
def test_shared_full_state_import_is_rejected(self):
|
||||
state = base_state()
|
||||
allowed, meta = can_import_full_state(state, {"id": "alice"})
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ ACTIVE = "active"
|
|||
VIOLATION_HIDDEN = "violation_hidden"
|
||||
|
||||
OBJECT_KEYS = {"static": "placed", "dynamic": "dynamicSummons"}
|
||||
DAY_MS = 10 * 60 * 1000
|
||||
|
||||
|
||||
def now_ms() -> int:
|
||||
|
|
@ -117,6 +118,87 @@ def _accepted(event: Dict[str, Any]) -> Dict[str, Any]:
|
|||
return {"accepted": True, "event": event}
|
||||
|
||||
|
||||
def phase_key_for_time(world_time_ms: int, day_ms: int = DAY_MS) -> str:
|
||||
progress = (int(world_time_ms) % int(day_ms or DAY_MS)) / float(day_ms or DAY_MS)
|
||||
if progress < 0.10:
|
||||
return "pre_dawn"
|
||||
if progress < 0.20:
|
||||
return "sunrise"
|
||||
if progress < 0.60:
|
||||
return "day"
|
||||
if progress < 0.72:
|
||||
return "sunset"
|
||||
return "night"
|
||||
|
||||
|
||||
def make_world_phase_event(at: Optional[int] = None, day_ms: int = DAY_MS) -> Dict[str, Any]:
|
||||
"""Create the authoritative day/night clock event emitted by the server."""
|
||||
stamp = at or now_ms()
|
||||
cycle = int(day_ms or DAY_MS)
|
||||
world_time_ms = int(stamp)
|
||||
progress = (world_time_ms % cycle) / float(cycle)
|
||||
return _server_event(
|
||||
"world.phase",
|
||||
"server",
|
||||
{
|
||||
"worldTimeMs": world_time_ms,
|
||||
"dayMs": cycle,
|
||||
"phase": {"key": phase_key_for_time(world_time_ms, cycle), "progress": progress},
|
||||
},
|
||||
stamp,
|
||||
)
|
||||
|
||||
|
||||
def make_dynamic_move_event(
|
||||
state: MutableMapping[str, Any],
|
||||
object_id: str,
|
||||
x: float,
|
||||
y: float,
|
||||
target_x: Optional[float] = None,
|
||||
target_y: Optional[float] = None,
|
||||
at: Optional[int] = None,
|
||||
actor: str = "server",
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Create an authoritative dynamic-object movement event.
|
||||
|
||||
Clients may ask for a target, but shared-world dynamic positions are only
|
||||
changed after this server event is applied.
|
||||
"""
|
||||
stamp = at or now_ms()
|
||||
kind, obj = find_object(state, object_id, "dynamic")
|
||||
if kind != "dynamic" or not obj:
|
||||
return None
|
||||
next_object = deepcopy(dict(obj))
|
||||
target = {
|
||||
"objectId": str(object_id),
|
||||
"x": float(x),
|
||||
"y": float(y),
|
||||
"targetX": float(target_x if target_x is not None else x),
|
||||
"targetY": float(target_y if target_y is not None else y),
|
||||
"homeX": float(next_object.get("homeX", x)),
|
||||
"homeY": float(next_object.get("homeY", y)),
|
||||
"serverAt": stamp,
|
||||
}
|
||||
next_object["serverState"] = target
|
||||
next_object["version"] = int(next_object.get("version") or 0) + 1
|
||||
return _server_event(
|
||||
"dynamic.move",
|
||||
actor or "server",
|
||||
{
|
||||
"objectId": str(object_id),
|
||||
"object": next_object,
|
||||
"x": target["x"],
|
||||
"y": target["y"],
|
||||
"targetX": target["targetX"],
|
||||
"targetY": target["targetY"],
|
||||
"homeX": target["homeX"],
|
||||
"homeY": target["homeY"],
|
||||
"objectVersion": next_object["version"],
|
||||
},
|
||||
stamp,
|
||||
)
|
||||
|
||||
|
||||
def _rejected(reason: str, **extra: Any) -> Dict[str, Any]:
|
||||
return {"accepted": False, "reason": reason, **extra}
|
||||
|
||||
|
|
@ -164,7 +246,7 @@ def apply_command(state: MutableMapping[str, Any], account: MutableMapping[str,
|
|||
if existing:
|
||||
allowed, meta = can_modify_object(state, account, str(obj["id"]))
|
||||
if not allowed:
|
||||
return _rejected(meta["reason"], **meta)
|
||||
return _rejected(meta["reason"], **{k: v for k, v in meta.items() if k != "reason"})
|
||||
owner = owner_of_object(state, existing)
|
||||
else:
|
||||
if str(asset.get("ownerAccountId") or "") != actor and not is_admin(account):
|
||||
|
|
@ -180,7 +262,7 @@ def apply_command(state: MutableMapping[str, Any], account: MutableMapping[str,
|
|||
kind = str(command.get("kind") or "")
|
||||
allowed, meta = can_modify_object(state, account, object_id)
|
||||
if not allowed:
|
||||
return _rejected(meta["reason"], **meta)
|
||||
return _rejected(meta["reason"], **{k: v for k, v in meta.items() if k != "reason"})
|
||||
found_kind, obj = find_object(state, object_id, kind if kind in OBJECT_KEYS else None)
|
||||
version = int((obj or {}).get("version") or 0) + 1
|
||||
tombstone = {"id": object_id, "deletedAt": at or now_ms(), "deletedBy": actor, "version": version}
|
||||
|
|
@ -190,7 +272,7 @@ def apply_command(state: MutableMapping[str, Any], account: MutableMapping[str,
|
|||
asset_id = str(command.get("assetId") or "")
|
||||
allowed, meta = can_delete_asset(state, account, asset_id)
|
||||
if not allowed:
|
||||
return _rejected(meta["reason"], **meta)
|
||||
return _rejected(meta["reason"], **{k: v for k, v in meta.items() if k != "reason"})
|
||||
asset = find_asset(state, asset_id) or {}
|
||||
version = int(asset.get("version") or 0) + 1
|
||||
object_tombstones: List[Dict[str, Any]] = []
|
||||
|
|
@ -224,6 +306,55 @@ def apply_server_event(state: MutableMapping[str, Any], event: MutableMapping[st
|
|||
normalize_tombstones(state)
|
||||
event_type = str(event.get("type") or "")
|
||||
|
||||
if event_type == "world.phase":
|
||||
clock = state.setdefault("serverSync", {}).setdefault("clock", {})
|
||||
day_ms = int(event.get("dayMs") or DAY_MS)
|
||||
world_time_ms = int(event.get("worldTimeMs") or event.get("serverNow") or event.get("serverAt") or now_ms())
|
||||
clock.clear()
|
||||
clock.update({
|
||||
"worldTimeMs": world_time_ms,
|
||||
"syncedAt": int(event.get("serverAt") or now_ms()),
|
||||
"dayMs": day_ms,
|
||||
"phase": event.get("phase") or {"key": phase_key_for_time(world_time_ms, day_ms), "progress": (world_time_ms % day_ms) / float(day_ms)},
|
||||
"serverEventId": event["serverEventId"],
|
||||
})
|
||||
state["serverSync"]["lastServerEventId"] = event["serverEventId"]
|
||||
return True
|
||||
|
||||
if event_type == "dynamic.move":
|
||||
object_id = str(event.get("objectId") or event.get("dynamicId") or "")
|
||||
obj = event.get("object")
|
||||
if not object_id and isinstance(obj, MutableMapping):
|
||||
object_id = str(obj.get("id") or "")
|
||||
if not object_id:
|
||||
return False
|
||||
object_version = int((obj or {}).get("version") or event.get("objectVersion") or 1) if isinstance(obj, MutableMapping) else int(event.get("objectVersion") or 1)
|
||||
if is_object_tombstoned(state, object_id, object_version):
|
||||
return False
|
||||
target = {
|
||||
"objectId": object_id,
|
||||
"x": float(event.get("x", (obj or {}).get("serverState", {}).get("x", (obj or {}).get("homeX", 0))) if isinstance(obj, MutableMapping) else event.get("x", 0)),
|
||||
"y": float(event.get("y", (obj or {}).get("serverState", {}).get("y", (obj or {}).get("homeY", 0))) if isinstance(obj, MutableMapping) else event.get("y", 0)),
|
||||
"targetX": float(event.get("targetX", event.get("x", 0))),
|
||||
"targetY": float(event.get("targetY", event.get("y", 0))),
|
||||
"homeX": float(event.get("homeX", (obj or {}).get("homeX", 0)) if isinstance(obj, MutableMapping) else event.get("homeX", 0)),
|
||||
"homeY": float(event.get("homeY", (obj or {}).get("homeY", 0)) if isinstance(obj, MutableMapping) else event.get("homeY", 0)),
|
||||
"serverAt": int(event.get("serverAt") or now_ms()),
|
||||
}
|
||||
server_sync = state.setdefault("serverSync", {})
|
||||
server_sync.setdefault("dynamicTargets", {})[object_id] = target
|
||||
if isinstance(obj, MutableMapping) and obj.get("id"):
|
||||
rows = state.setdefault("dynamicSummons", [])
|
||||
next_object = deepcopy(dict(obj))
|
||||
next_object["serverState"] = target
|
||||
index = next((i for i, item in enumerate(rows) if str(item.get("id") or "") == object_id), -1)
|
||||
if index >= 0:
|
||||
rows[index] = next_object
|
||||
else:
|
||||
rows.append(next_object)
|
||||
server_sync["lastServerEventId"] = event["serverEventId"]
|
||||
return True
|
||||
|
||||
if event_type == "asset.upsert":
|
||||
asset = event.get("asset")
|
||||
if not isinstance(asset, MutableMapping) or not asset.get("id"):
|
||||
|
|
@ -265,6 +396,7 @@ def apply_server_event(state: MutableMapping[str, Any], event: MutableMapping[st
|
|||
state[OBJECT_KEYS[kind]] = [obj for obj in state.get(OBJECT_KEYS[kind]) or [] if str(obj.get("id") or "") != object_id]
|
||||
state.setdefault("objectVotes", {}).pop(object_id, None)
|
||||
state.setdefault("hiddenObjects", {}).pop(object_id, None)
|
||||
state.setdefault("serverSync", {}).setdefault("dynamicTargets", {}).pop(object_id, None)
|
||||
state["moderationReports"] = [report for report in state.get("moderationReports") or [] if report.get("objectId") != object_id]
|
||||
state.setdefault("serverSync", {})["lastServerEventId"] = event["serverEventId"]
|
||||
return True
|
||||
|
|
@ -283,9 +415,11 @@ def apply_server_event(state: MutableMapping[str, Any], event: MutableMapping[st
|
|||
state[key] = [obj for obj in state.get(key) or [] if str(obj.get("id") or "") not in removed_object_ids]
|
||||
state.setdefault("assetVotes", {}).pop(asset_id, None)
|
||||
state.setdefault("hiddenAssets", {}).pop(asset_id, None)
|
||||
dynamic_targets = state.setdefault("serverSync", {}).setdefault("dynamicTargets", {})
|
||||
for object_id in removed_object_ids:
|
||||
state.setdefault("objectVotes", {}).pop(object_id, None)
|
||||
state.setdefault("hiddenObjects", {}).pop(object_id, None)
|
||||
dynamic_targets.pop(object_id, None)
|
||||
state["moderationReports"] = [report for report in state.get("moderationReports") or [] if report.get("objectId") not in removed_object_ids]
|
||||
state.setdefault("serverSync", {})["lastServerEventId"] = event["serverEventId"]
|
||||
return True
|
||||
|
|
|
|||
630
styles.css
630
styles.css
|
|
@ -1896,3 +1896,633 @@ body, button, input, select, textarea { font-size: 15px; }
|
|||
max-width: 640px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* v22 requested: compact left drawer + Create tab header layout
|
||||
Desktop drawer is capped to roughly 40% of the viewport instead of occupying most of the screen. */
|
||||
@media (min-width: 761px) {
|
||||
.studioDrawer {
|
||||
width: clamp(440px, 40vw, 620px) !important;
|
||||
}
|
||||
|
||||
.creationMetaRow {
|
||||
display: grid !important;
|
||||
grid-template-columns: minmax(150px, 1fr) 58px 14px 58px minmax(96px, .64fr) !important;
|
||||
gap: 8px !important;
|
||||
align-items: center !important;
|
||||
margin: 2px 0 10px !important;
|
||||
}
|
||||
|
||||
.creationMetaRow .metaRole {
|
||||
grid-column: auto !important;
|
||||
}
|
||||
|
||||
.creationMetaRow .metaInput,
|
||||
.creationMetaRow select.metaInput {
|
||||
height: 46px !important;
|
||||
min-width: 0 !important;
|
||||
margin: 0 !important;
|
||||
border-width: 2px !important;
|
||||
font-size: 16px !important;
|
||||
font-weight: 850 !important;
|
||||
letter-spacing: .02em !important;
|
||||
padding: 7px 10px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.creationMetaRow .metaDimension {
|
||||
padding-left: 4px !important;
|
||||
padding-right: 4px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.creationMetaRow .dimensionSeparator {
|
||||
display: grid !important;
|
||||
place-items: center !important;
|
||||
color: #e33a4d !important;
|
||||
font-size: 18px !important;
|
||||
font-weight: 950 !important;
|
||||
}
|
||||
|
||||
.paintLayout {
|
||||
display: grid !important;
|
||||
grid-template-columns: minmax(0, 1fr) clamp(92px, 9.2vw, 128px) !important;
|
||||
gap: 10px !important;
|
||||
align-items: start !important;
|
||||
margin: 0 0 10px !important;
|
||||
}
|
||||
|
||||
#paintCanvas {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
aspect-ratio: 1 / 1 !important;
|
||||
justify-self: stretch !important;
|
||||
}
|
||||
|
||||
.palettePanel {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
min-width: 0 !important;
|
||||
align-self: stretch !important;
|
||||
justify-self: stretch !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.paletteGrid {
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
|
||||
gap: 3px !important;
|
||||
padding: 5px !important;
|
||||
margin: 0 !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.paletteSwatch,
|
||||
.paletteGrid button {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
min-height: 0 !important;
|
||||
aspect-ratio: 1 / 1 !important;
|
||||
}
|
||||
|
||||
.editorToolRow,
|
||||
.editorHistoryRow,
|
||||
.advancedRow,
|
||||
#editHint {
|
||||
max-width: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 761px) and (max-width: 1100px) {
|
||||
.studioDrawer {
|
||||
width: min(440px, calc(100vw - 20px)) !important;
|
||||
}
|
||||
|
||||
.creationMetaRow {
|
||||
grid-template-columns: minmax(126px, 1fr) 50px 12px 50px minmax(84px, .58fr) !important;
|
||||
gap: 6px !important;
|
||||
}
|
||||
|
||||
.creationMetaRow .metaInput,
|
||||
.creationMetaRow select.metaInput {
|
||||
height: 42px !important;
|
||||
font-size: 14px !important;
|
||||
padding: 6px 8px !important;
|
||||
}
|
||||
|
||||
.paintLayout {
|
||||
grid-template-columns: minmax(0, 1fr) 92px !important;
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
.paletteGrid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
|
||||
gap: 2px !important;
|
||||
padding: 4px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* v23 requested: canvas-surroundings redesign + palette cleanup */
|
||||
:root {
|
||||
--editorSurface: #efe9dc;
|
||||
--editorPanel: #f6f1e5;
|
||||
--editorLine: #314159;
|
||||
--editorMuted: #9a9aa0;
|
||||
--editorAccent: #ef4d63;
|
||||
--editorActive: #f28aaa;
|
||||
}
|
||||
|
||||
.card.editorCard.heroEditor {
|
||||
background: var(--editorSurface) !important;
|
||||
}
|
||||
|
||||
.compactEditorTop {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.creationMetaRow .metaInput,
|
||||
.creationMetaRow select.metaInput {
|
||||
background: var(--editorPanel) !important;
|
||||
border-color: var(--editorLine) !important;
|
||||
color: var(--editorLine) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.creationMetaRow input::placeholder,
|
||||
.creationMetaRow select:invalid {
|
||||
color: var(--editorMuted) !important;
|
||||
}
|
||||
.creationMetaRow .metaRole {
|
||||
text-align: center !important;
|
||||
text-align-last: center !important;
|
||||
}
|
||||
|
||||
.paintLayout {
|
||||
align-items: stretch !important;
|
||||
}
|
||||
|
||||
#paintCanvas {
|
||||
display: block !important;
|
||||
background: #ece8df !important;
|
||||
border: 6px solid var(--editorLine) !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.palettePanel {
|
||||
background: var(--editorPanel) !important;
|
||||
border: 5px solid rgba(49, 65, 89, .28) !important;
|
||||
padding: 10px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.paletteGrid {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
gap: 6px !important;
|
||||
align-content: start !important;
|
||||
}
|
||||
|
||||
.paletteSwatch,
|
||||
.paletteGrid button {
|
||||
position: relative !important;
|
||||
border: 4px solid rgba(76, 76, 76, .45) !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.paletteSwatch::after,
|
||||
.paletteGrid button::after {
|
||||
content: attr(data-code) !important;
|
||||
position: absolute !important;
|
||||
right: 3px !important;
|
||||
bottom: 0 !important;
|
||||
font-size: 10px !important;
|
||||
line-height: 1 !important;
|
||||
font-weight: 800 !important;
|
||||
color: rgba(30, 40, 55, .66) !important;
|
||||
text-shadow: 0 1px 0 rgba(255,255,255,.52) !important;
|
||||
}
|
||||
|
||||
.paletteSwatch.active,
|
||||
.paletteGrid button.active {
|
||||
border-color: var(--editorActive) !important;
|
||||
box-shadow: inset 0 0 0 5px rgba(255, 192, 208, .92) !important;
|
||||
}
|
||||
|
||||
.basicEditorTools,
|
||||
.editorUtilityRow {
|
||||
display: grid !important;
|
||||
gap: 0 !important;
|
||||
margin: 0 0 14px !important;
|
||||
border: 5px solid var(--editorAccent) !important;
|
||||
background: var(--editorSurface) !important;
|
||||
}
|
||||
|
||||
.basicEditorTools {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
|
||||
}
|
||||
|
||||
.editorUtilityRow {
|
||||
grid-template-columns: minmax(0, 1.14fr) repeat(2, minmax(0, 1fr)) !important;
|
||||
}
|
||||
|
||||
.basicEditorTools .tool,
|
||||
.editorUtilityRow .tool {
|
||||
min-height: 98px !important;
|
||||
border: none !important;
|
||||
border-right: 5px solid var(--editorAccent) !important;
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
color: #7f7d7a !important;
|
||||
font-size: clamp(20px, 2.4vw, 38px) !important;
|
||||
font-weight: 500 !important;
|
||||
letter-spacing: .01em !important;
|
||||
text-transform: none !important;
|
||||
}
|
||||
.basicEditorTools .tool:last-child,
|
||||
.editorUtilityRow .tool:last-child {
|
||||
border-right: none !important;
|
||||
}
|
||||
.basicEditorTools .tool.active,
|
||||
.editorUtilityRow .tool.active {
|
||||
background: rgba(255,255,255,.24) !important;
|
||||
color: #434951 !important;
|
||||
}
|
||||
.basicEditorTools .tool:disabled,
|
||||
.editorUtilityRow .tool:disabled {
|
||||
color: rgba(127,125,122,.55) !important;
|
||||
}
|
||||
|
||||
.advancedPanel {
|
||||
border: 5px solid var(--editorAccent) !important;
|
||||
background: #ececeb !important;
|
||||
padding: 16px 16px 12px !important;
|
||||
margin-top: 2px !important;
|
||||
}
|
||||
|
||||
.advancedPanelHeader {
|
||||
color: #7f7d7a !important;
|
||||
font-size: clamp(28px, 3vw, 54px) !important;
|
||||
line-height: 1.05 !important;
|
||||
letter-spacing: .01em !important;
|
||||
margin: 0 0 10px !important;
|
||||
}
|
||||
|
||||
.advancedPanel .advancedDrawRow,
|
||||
.advancedPanel .advancedActionRow,
|
||||
.advancedPanel .extraAdvancedRow {
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
|
||||
.advancedPanel .toolRow {
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
.advancedPanel .tool,
|
||||
.advancedPanel select,
|
||||
.advancedPanel .particleControl {
|
||||
border-width: 3px !important;
|
||||
border-color: rgba(49, 65, 89, .78) !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.advancedPanel .hint {
|
||||
display: block !important;
|
||||
color: #5d6570 !important;
|
||||
font-size: 13px !important;
|
||||
line-height: 1.45 !important;
|
||||
}
|
||||
|
||||
#editHint {
|
||||
margin: 2px 0 0 !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
@media (min-width: 761px) {
|
||||
.studioDrawer {
|
||||
width: min(40vw, 1024px) !important;
|
||||
max-width: calc(100vw - 20px) !important;
|
||||
}
|
||||
|
||||
.creationMetaRow {
|
||||
grid-template-columns: minmax(170px, 1.5fr) 78px 26px 78px minmax(110px, .96fr) !important;
|
||||
gap: 16px !important;
|
||||
margin: 0 0 18px !important;
|
||||
}
|
||||
|
||||
.creationMetaRow .metaInput,
|
||||
.creationMetaRow select.metaInput {
|
||||
height: 92px !important;
|
||||
padding: 14px 20px !important;
|
||||
border-width: 4px !important;
|
||||
font-size: 32px !important;
|
||||
font-weight: 800 !important;
|
||||
}
|
||||
|
||||
.creationMetaRow .dimensionSeparator {
|
||||
font-size: 52px !important;
|
||||
}
|
||||
|
||||
.paintLayout {
|
||||
grid-template-columns: minmax(0, 1fr) 255px !important;
|
||||
gap: 22px !important;
|
||||
margin-bottom: 18px !important;
|
||||
}
|
||||
|
||||
.paletteGrid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
|
||||
}
|
||||
|
||||
.paletteSwatch,
|
||||
.paletteGrid button {
|
||||
aspect-ratio: 1 / 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.paintLayout {
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 12px !important;
|
||||
}
|
||||
|
||||
.palettePanel {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.paletteGrid {
|
||||
grid-template-columns: repeat(8, minmax(0, 1fr)) !important;
|
||||
}
|
||||
|
||||
.basicEditorTools .tool,
|
||||
.editorUtilityRow .tool {
|
||||
min-height: 64px !important;
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
.advancedPanelHeader {
|
||||
font-size: 26px !important;
|
||||
}
|
||||
|
||||
.creationMetaRow {
|
||||
grid-template-columns: minmax(0, 1fr) 58px 14px 58px !important;
|
||||
gap: 8px !important;
|
||||
}
|
||||
.creationMetaRow .metaRole {
|
||||
grid-column: 1 / -1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* v24: align canvas, primary tools, and palette exactly as one editor block */
|
||||
.paintMainColumn {
|
||||
display: grid !important;
|
||||
grid-template-rows: auto auto auto !important;
|
||||
gap: 14px !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
.paintMainColumn #paintCanvas {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
.basicEditorTools,
|
||||
.editorUtilityRow {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.advancedPanel {
|
||||
min-height: 174px !important;
|
||||
}
|
||||
@media (min-width: 761px) {
|
||||
.paintLayout {
|
||||
align-items: start !important;
|
||||
}
|
||||
.palettePanel {
|
||||
min-height: 100% !important;
|
||||
}
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.paintMainColumn {
|
||||
gap: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* v25: requested layout-only cleanup. Keep existing button design; move controls around the canvas. */
|
||||
:root {
|
||||
--createGap: 12px;
|
||||
}
|
||||
|
||||
.card.editorCard.heroEditor {
|
||||
background: var(--panel) !important;
|
||||
}
|
||||
|
||||
@media (min-width: 761px) {
|
||||
.studioDrawer {
|
||||
width: clamp(430px, 40vw, 760px) !important;
|
||||
max-width: calc(100vw - 20px) !important;
|
||||
}
|
||||
|
||||
.creationMetaRow {
|
||||
grid-template-columns: minmax(136px, 1.35fr) clamp(52px, 5vw, 76px) 20px clamp(52px, 5vw, 76px) minmax(94px, .92fr) !important;
|
||||
gap: 12px !important;
|
||||
margin: 0 0 var(--createGap) !important;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
|
||||
.creationMetaRow .metaInput,
|
||||
.creationMetaRow select.metaInput {
|
||||
height: 54px !important;
|
||||
min-height: 0 !important;
|
||||
border: 2px solid var(--line) !important;
|
||||
background: #fffdf5 !important;
|
||||
box-shadow: 3px 3px 0 rgba(36, 48, 68, .14) !important;
|
||||
color: var(--ink) !important;
|
||||
font-size: 15px !important;
|
||||
font-weight: 850 !important;
|
||||
padding: 9px 10px !important;
|
||||
}
|
||||
|
||||
.creationMetaRow .metaDimension {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.creationMetaRow .dimensionSeparator {
|
||||
display: grid !important;
|
||||
place-items: center !important;
|
||||
color: var(--danger) !important;
|
||||
font-size: 26px !important;
|
||||
line-height: 1 !important;
|
||||
font-weight: 950 !important;
|
||||
}
|
||||
|
||||
.paintLayout {
|
||||
grid-template-columns: minmax(0, 1fr) clamp(104px, 10.5vw, 156px) !important;
|
||||
gap: 14px !important;
|
||||
align-items: start !important;
|
||||
margin-bottom: 14px !important;
|
||||
}
|
||||
|
||||
.paintMainColumn {
|
||||
display: grid !important;
|
||||
grid-template-rows: auto auto auto !important;
|
||||
gap: 10px !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.paintMainColumn #paintCanvas {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
aspect-ratio: 1 / 1 !important;
|
||||
border: 3px solid var(--line) !important;
|
||||
background:
|
||||
linear-gradient(45deg, #f4ebdc 25%, transparent 25%) 0 0/16px 16px,
|
||||
linear-gradient(45deg, transparent 75%, #f4ebdc 75%) 0 0/16px 16px,
|
||||
#fffaf0 !important;
|
||||
}
|
||||
|
||||
.palettePanel {
|
||||
width: 100% !important;
|
||||
align-self: stretch !important;
|
||||
background: #fff7de !important;
|
||||
border: 2px solid rgba(36, 48, 68, .28) !important;
|
||||
padding: 8px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.paletteGrid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
|
||||
gap: 4px !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
align-content: start !important;
|
||||
}
|
||||
|
||||
.paletteSwatch,
|
||||
.paletteGrid button {
|
||||
border: 2px solid rgba(36, 48, 68, .42) !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
aspect-ratio: 1 / 1 !important;
|
||||
}
|
||||
|
||||
.paletteSwatch.active,
|
||||
.paletteGrid button.active {
|
||||
border-color: var(--danger) !important;
|
||||
box-shadow: inset 0 0 0 3px rgba(255,255,255,.55) !important;
|
||||
outline: 2px solid rgba(36, 48, 68, .65) !important;
|
||||
outline-offset: -5px !important;
|
||||
}
|
||||
|
||||
.basicEditorTools,
|
||||
.editorUtilityRow {
|
||||
display: grid !important;
|
||||
gap: 8px !important;
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.basicEditorTools {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
|
||||
}
|
||||
|
||||
.editorUtilityRow {
|
||||
grid-template-columns: minmax(0, 1.12fr) repeat(2, minmax(0, 1fr)) !important;
|
||||
}
|
||||
|
||||
.basicEditorTools .tool,
|
||||
.editorUtilityRow .tool {
|
||||
min-height: 42px !important;
|
||||
border: 2px solid var(--line) !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: 3px 3px 0 rgba(36, 48, 68, .16) !important;
|
||||
color: var(--ink) !important;
|
||||
font-size: 14px !important;
|
||||
line-height: 1.15 !important;
|
||||
font-weight: 850 !important;
|
||||
padding: 8px 7px !important;
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
||||
.basicEditorTools .tool:active,
|
||||
.editorUtilityRow .tool:active {
|
||||
transform: translate(2px, 2px) !important;
|
||||
box-shadow: 1px 1px 0 rgba(36, 48, 68, .16) !important;
|
||||
}
|
||||
|
||||
.advancedPanel {
|
||||
margin-top: 0 !important;
|
||||
padding: 12px !important;
|
||||
border: 2px solid var(--line) !important;
|
||||
background: var(--panel-2) !important;
|
||||
min-height: 0 !important;
|
||||
box-shadow: 4px 4px 0 rgba(36, 48, 68, .12) !important;
|
||||
}
|
||||
|
||||
.advancedPanelHeader {
|
||||
font-size: 16px !important;
|
||||
font-weight: 900 !important;
|
||||
line-height: 1.2 !important;
|
||||
color: var(--ink) !important;
|
||||
margin: 0 0 8px !important;
|
||||
}
|
||||
|
||||
.advancedPanel .advancedDrawRow,
|
||||
.advancedPanel .advancedActionRow,
|
||||
.advancedPanel .extraAdvancedRow {
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.advancedPanel .hint {
|
||||
font-size: 12px !important;
|
||||
line-height: 1.45 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 761px) and (max-width: 1180px) {
|
||||
.creationMetaRow {
|
||||
grid-template-columns: minmax(120px, 1.25fr) 48px 16px 48px minmax(82px, .86fr) !important;
|
||||
gap: 8px !important;
|
||||
}
|
||||
.creationMetaRow .metaInput,
|
||||
.creationMetaRow select.metaInput {
|
||||
height: 46px !important;
|
||||
font-size: 13px !important;
|
||||
padding: 7px 8px !important;
|
||||
}
|
||||
.creationMetaRow .dimensionSeparator {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
.paintLayout {
|
||||
grid-template-columns: minmax(0, 1fr) 108px !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
.paletteGrid {
|
||||
gap: 3px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.basicEditorTools,
|
||||
.editorUtilityRow {
|
||||
gap: 8px !important;
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
.basicEditorTools .tool,
|
||||
.editorUtilityRow .tool {
|
||||
min-height: 40px !important;
|
||||
border: 2px solid var(--line) !important;
|
||||
box-shadow: 3px 3px 0 rgba(36, 48, 68, .16) !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 850 !important;
|
||||
}
|
||||
.advancedPanel {
|
||||
border: 2px solid var(--line) !important;
|
||||
background: var(--panel-2) !important;
|
||||
padding: 10px !important;
|
||||
}
|
||||
.advancedPanelHeader {
|
||||
font-size: 15px !important;
|
||||
font-weight: 900 !important;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue