fiirst
This commit is contained in:
parent
4f5cf7d425
commit
2d42f7f487
7 changed files with 4065 additions and 0 deletions
185
index.html
Normal file
185
index.html
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Pixel Island Summoner - Local Prototype</title>
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<canvas id="worldCanvas" aria-label="Island map"></canvas>
|
||||
|
||||
<button id="openEditor" class="edgeAdd" title="Open Pixel Studio">+</button>
|
||||
|
||||
<header class="hud topHud">
|
||||
<div>
|
||||
<div class="logo">Pixel Island</div>
|
||||
<div class="subline">Local prototype / Draw pixels, summon them to islands.</div>
|
||||
</div>
|
||||
<label class="authorCard">
|
||||
<span>Your Name</span>
|
||||
<input id="authorName" type="text" maxlength="24" value="Local Artist" />
|
||||
</label>
|
||||
<div class="clockCard" aria-label="world clock">
|
||||
<div id="phaseLabel" class="phaseLabel">Day</div>
|
||||
<div class="phaseBar"><span id="phaseBar"></span></div>
|
||||
<div class="clockHint">10 min = 1 day</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<aside class="hud toolHud">
|
||||
<button id="modeInspect" class="iconButton active" title="Pan / Inspect">Pan</button>
|
||||
<button id="modePlace" class="iconButton" title="Summon selected asset">Place</button>
|
||||
<button id="modeErase" class="iconButton danger" title="Remove asset">Erase</button>
|
||||
<div class="divider"></div>
|
||||
<button id="zoomOut" class="iconButton" title="Zoom out">-</button>
|
||||
<button id="zoomIn" class="iconButton" title="Zoom in">+</button>
|
||||
<button id="resetView" class="iconButton" title="Reset view">Reset</button>
|
||||
<button id="settingsToggle" class="iconButton" title="World settings">Settings</button>
|
||||
</aside>
|
||||
|
||||
<section id="settingsPanel" class="hud settingsPanel" hidden aria-label="World settings">
|
||||
<label class="toggleLine">
|
||||
<input id="lightingToggle" type="checkbox" checked />
|
||||
Lighting
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section id="studioDrawer" class="studioDrawer" aria-label="Pixel Studio">
|
||||
<div class="drawerHead">
|
||||
<div>
|
||||
<h1>Pixel Studio</h1>
|
||||
<p>Make static scenery or moving island life.</p>
|
||||
</div>
|
||||
<button id="closeEditor" class="ghostButton" title="Close">x</button>
|
||||
</div>
|
||||
|
||||
<nav class="tabs" aria-label="Studio tabs">
|
||||
<button class="tab active" data-tab="draw">Draw</button>
|
||||
<button class="tab" data-tab="library">Library</button>
|
||||
<button class="tab" data-tab="data">Data</button>
|
||||
</nav>
|
||||
|
||||
<div class="drawerBody">
|
||||
<section id="tab-draw" class="tabPanel active">
|
||||
<div class="card editorCard heroEditor">
|
||||
<div class="quickSetupBar twoCols">
|
||||
<label class="field">Canvas size
|
||||
<select id="assetSize">
|
||||
<option value="8" selected>8 x 8</option>
|
||||
<option value="16">16 x 16</option>
|
||||
<option value="32">32 x 32</option>
|
||||
<option value="64">64 x 64</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">Role
|
||||
<select id="assetCategory">
|
||||
<option value="human">Human</option>
|
||||
<option value="animal">Animal</option>
|
||||
<option value="nature" selected>Nature</option>
|
||||
<option value="building">Building</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="field compactNameField">Name
|
||||
<input id="assetName" type="text" maxlength="32" placeholder="Tiny bakery, round tree, island pup..." />
|
||||
</label>
|
||||
<div class="editorTop compactEditorTop">
|
||||
<div id="sideSwitcher" class="sideSwitcher" hidden aria-label="Sprite direction">
|
||||
<button id="editLeft" title="Left-facing sprite">Left</button>
|
||||
<button id="editRight" class="active" title="Right-facing sprite is the main/front direction">Right</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="paintLayout">
|
||||
<canvas id="paintCanvas" width="512" height="512" aria-label="Pixel editor"></canvas>
|
||||
<div class="palettePanel">
|
||||
<div id="paletteGrid" class="paletteGrid" aria-label="Color palette"></div>
|
||||
<div class="outlineControls">
|
||||
<button id="setOutline" class="tool" type="button">Set Outline</button>
|
||||
<button id="clearOutline" class="tool" type="button">No Outline</button>
|
||||
<span id="outlineStatus">Outline: none</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolRow">
|
||||
<button id="toolBrush" class="tool active">Draw</button>
|
||||
<button id="toolErase" class="tool">Erase</button>
|
||||
<button id="toolFill" class="tool">Fill</button>
|
||||
<button id="toolPick" class="tool">Pick</button>
|
||||
<button id="toolLight" class="tool">Light</button>
|
||||
<button id="toolDoor" class="tool buildingOnly">Door</button>
|
||||
<button id="clearPaint" class="tool danger">Clear</button>
|
||||
</div>
|
||||
<div class="toolRow editorActions">
|
||||
<button id="undoPaint" class="tool" type="button">Undo</button>
|
||||
<button id="redoPaint" class="tool" type="button">Redo</button>
|
||||
<button id="mirrorLeft" class="tool" type="button">Mirror Left</button>
|
||||
</div>
|
||||
<input id="paintColor" type="color" value="#6bd06b" hidden />
|
||||
<div id="editHint" class="hint">Palette color is used for both pixels and light cells.</div>
|
||||
</div>
|
||||
|
||||
<div class="card stack summonCard">
|
||||
<div class="cardTitle">Finish</div>
|
||||
<div class="actionRow finishActions">
|
||||
<button id="saveAndPlace" class="primary bigPrimary">Save + Summon on Map</button>
|
||||
<button id="saveAsset" class="secondary">Save only</button>
|
||||
<button id="newAsset" class="secondary">New</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="lineageNote" class="lineageNote"></div>
|
||||
</section>
|
||||
|
||||
<section id="tab-library" class="tabPanel">
|
||||
<div class="card stack">
|
||||
<div class="cardTitle">Library</div>
|
||||
<p class="hint">Click an asset to select it and jump to it on the map. Copy Edit creates a new derivative.</p>
|
||||
<div id="assetList" class="assetList"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="tab-data" class="tabPanel">
|
||||
<div class="card stack">
|
||||
<div class="cardTitle">Local save</div>
|
||||
<p class="hint">Saved to this browser only. Export JSON if you want to move or share a local world.</p>
|
||||
<div class="actionRow wrap">
|
||||
<button id="exportData">Export JSON</button>
|
||||
<button id="importData">Import JSON</button>
|
||||
<button id="resetAll" class="danger">Reset all</button>
|
||||
</div>
|
||||
<textarea id="dataBox" rows="10" placeholder="Exported JSON appears here."></textarea>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="selectionBubble" class="selectionBubble" hidden>
|
||||
<div id="bubbleName" class="bubbleName">Name</div>
|
||||
<div id="bubbleAuthor" class="bubbleAuthor">by Author</div>
|
||||
<div id="bubbleRemixFrom" class="bubbleRemixFrom" hidden></div>
|
||||
<div id="bubbleRemixCount" class="bubbleRemixCount">Remixed: 0</div>
|
||||
<div class="bubbleVotes">
|
||||
<button id="voteUp" type="button">Up</button>
|
||||
<span id="voteScore">0</span>
|
||||
<button id="voteDown" type="button">Down</button>
|
||||
</div>
|
||||
<div class="bubbleActions">
|
||||
<button id="bubbleRemix" type="button">Remix</button>
|
||||
<button id="bubbleHide" type="button" hidden>Hide</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="toast" class="toast" hidden></div>
|
||||
</div>
|
||||
|
||||
<script src="./js/vector.js"></script>
|
||||
<script src="./js/lighting.js"></script>
|
||||
<script src="./js/render-pipeline.js"></script>
|
||||
<script src="./js/editor-actions.js"></script>
|
||||
<script src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
12
js/editor-actions.js
Normal file
12
js/editor-actions.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
const root = window.PixelIslandModules ||= {};
|
||||
|
||||
root.EditorActions = {
|
||||
apply(snapshot, pushHistory, mutate) {
|
||||
pushHistory(snapshot());
|
||||
mutate();
|
||||
}
|
||||
};
|
||||
})();
|
||||
68
js/lighting.js
Normal file
68
js/lighting.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
const root = window.PixelIslandModules ||= {};
|
||||
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
||||
const lerp = (a, b, t) => a + (b - a) * t;
|
||||
const mod = (n, m) => ((n % m) + m) % m;
|
||||
|
||||
function getCelestialLightForMinute(minute) {
|
||||
const isNight = minute >= 6;
|
||||
const local = isNight ? (minute - 6) / 4 : minute / 6;
|
||||
const t = clamp(local, 0, 1);
|
||||
const eased = t * t * (3 - 2 * t);
|
||||
const elevation = Math.max(.08, Math.sin(t * Math.PI));
|
||||
const sourceX = isNight ? lerp(-1.05, 1.05, eased) : lerp(1.18, -1.18, eased);
|
||||
const sourceY = isNight ? -.46 - elevation * .24 : -.58 - elevation * .34;
|
||||
return {
|
||||
x: sourceX,
|
||||
y: sourceY,
|
||||
elevation,
|
||||
isNight,
|
||||
shadeAlpha: isNight ? .18 : lerp(.20, .08, elevation)
|
||||
};
|
||||
}
|
||||
|
||||
function getShadowForMinute(minute) {
|
||||
const light = getCelestialLightForMinute(minute);
|
||||
const length = lerp(light.isNight ? 1.35 : 1.9, light.isNight ? .85 : .52, light.elevation);
|
||||
return {
|
||||
x: -light.x * 6.5 * length,
|
||||
y: Math.min(-1.8, light.y * 4.2 * length),
|
||||
length,
|
||||
alpha: light.isNight ? .085 : lerp(.27, .12, light.elevation)
|
||||
};
|
||||
}
|
||||
|
||||
function getPhase(dayMs, now = Date.now()) {
|
||||
const t = mod(now, dayMs);
|
||||
const minute = t / 60000;
|
||||
const stops = [
|
||||
{ at: 0, key: 'morning', label: 'Morning', darkness: 0.18, tint: [255, 208, 144, 0.12] },
|
||||
{ at: 1, key: 'day', label: 'Day', darkness: 0.00, tint: [255, 255, 255, 0.00] },
|
||||
{ at: 5, key: 'day', label: 'Day', darkness: 0.00, tint: [255, 255, 255, 0.00] },
|
||||
{ at: 6, key: 'evening', label: 'Evening', darkness: 0.18, tint: [255, 146, 114, 0.14] },
|
||||
{ at: 10, key: 'night', label: 'Night', darkness: 0.48, tint: [36, 47, 96, 0.18] }
|
||||
];
|
||||
let a = stops[0], b = stops[1];
|
||||
for (let i = 0; i < stops.length - 1; i++) {
|
||||
if (minute >= stops[i].at && minute < stops[i + 1].at) { a = stops[i]; b = stops[i + 1]; break; }
|
||||
if (minute >= 6) { a = stops[3]; b = stops[4]; }
|
||||
}
|
||||
const localT = clamp((minute - a.at) / Math.max(0.0001, b.at - a.at), 0, 1);
|
||||
const eased = localT * localT * (3 - 2 * localT);
|
||||
const tint = a.tint.map((v, i) => lerp(v, b.tint[i], eased));
|
||||
const label = minute < 1 ? 'Morning' : minute < 5 ? 'Day' : minute < 6 ? 'Evening' : 'Night';
|
||||
return {
|
||||
key: label.toLowerCase(),
|
||||
label,
|
||||
progress: t / dayMs,
|
||||
darkness: lerp(a.darkness, b.darkness, eased),
|
||||
tint: `rgba(${Math.round(tint[0])}, ${Math.round(tint[1])}, ${Math.round(tint[2])}, ${tint[3].toFixed(3)})`,
|
||||
light: getCelestialLightForMinute(minute),
|
||||
shadow: getShadowForMinute(minute)
|
||||
};
|
||||
}
|
||||
|
||||
root.Lighting = { getPhase, getShadowForMinute, getCelestialLightForMinute };
|
||||
})();
|
||||
11
js/render-pipeline.js
Normal file
11
js/render-pipeline.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
const root = window.PixelIslandModules ||= {};
|
||||
|
||||
root.RenderPipeline = {
|
||||
run(stages, context) {
|
||||
for (const stage of stages) stage(context);
|
||||
}
|
||||
};
|
||||
})();
|
||||
24
js/vector.js
Normal file
24
js/vector.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
const root = window.PixelIslandModules ||= {};
|
||||
|
||||
root.Vec2 = {
|
||||
add(a, b) {
|
||||
return { x: a.x + b.x, y: a.y + b.y };
|
||||
},
|
||||
sub(a, b) {
|
||||
return { x: a.x - b.x, y: a.y - b.y };
|
||||
},
|
||||
scale(v, amount) {
|
||||
return { x: v.x * amount, y: v.y * amount };
|
||||
},
|
||||
length(v) {
|
||||
return Math.hypot(v.x, v.y);
|
||||
},
|
||||
normalize(v) {
|
||||
const length = Math.hypot(v.x, v.y) || 1;
|
||||
return { x: v.x / length, y: v.y / length };
|
||||
}
|
||||
};
|
||||
})();
|
||||
818
styles.css
Normal file
818
styles.css
Normal file
|
|
@ -0,0 +1,818 @@
|
|||
:root {
|
||||
--ink: #243044;
|
||||
--muted: #6f7b91;
|
||||
--paper: #fff7e8;
|
||||
--panel: rgba(255, 248, 232, .94);
|
||||
--panel-solid: #fff8e9;
|
||||
--panel-2: #ffeec9;
|
||||
--line: #2d3c50;
|
||||
--line-soft: rgba(45, 60, 80, .18);
|
||||
--pink: #ff85b3;
|
||||
--pink-dark: #f05f98;
|
||||
--mint: #73d6a4;
|
||||
--sky: #77c9ff;
|
||||
--sun: #ffd66e;
|
||||
--danger: #ff6b6b;
|
||||
--shadow: 6px 6px 0 rgba(34, 45, 63, .14);
|
||||
--hard-shadow: 4px 4px 0 rgba(34, 45, 63, .22);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body { height: 100%; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
background: #92d8ff;
|
||||
font-family: "Courier New", "Monaco", "Lucida Console", ui-monospace, monospace;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button, input, select, textarea { font: inherit; }
|
||||
button { -webkit-tap-highlight-color: transparent; }
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: .48;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.app {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#worldCanvas {
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
image-rendering: pixelated;
|
||||
cursor: grab;
|
||||
background: #8cd4ff;
|
||||
}
|
||||
|
||||
#worldCanvas.dragging { cursor: grabbing; }
|
||||
#worldCanvas.placeCursor { cursor: copy; }
|
||||
#worldCanvas.eraseCursor { cursor: not-allowed; }
|
||||
|
||||
.hud {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
background: var(--panel);
|
||||
border: 2px solid var(--line);
|
||||
border-radius: 0;
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.topHud {
|
||||
top: 14px;
|
||||
left: 14px;
|
||||
right: 14px;
|
||||
min-height: 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 12px 14px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-weight: 900;
|
||||
letter-spacing: .04em;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
display: inline-block;
|
||||
background: var(--pink);
|
||||
border: 2px solid var(--line);
|
||||
padding: 1px 6px;
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
transform: rotate(-2deg);
|
||||
}
|
||||
|
||||
.subline {
|
||||
margin-top: 3px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
|
||||
.authorCard {
|
||||
width: 190px;
|
||||
border: 2px solid var(--line);
|
||||
background: #fffdf5;
|
||||
padding: 6px 8px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.authorCard span {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
font-weight: 900;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.authorCard input {
|
||||
margin-top: 3px;
|
||||
padding: 5px 6px;
|
||||
border-width: 2px;
|
||||
background: #fffaf0;
|
||||
}
|
||||
|
||||
.clockCard {
|
||||
width: 230px;
|
||||
border: 2px solid var(--line);
|
||||
background: #fffdf5;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.phaseLabel {
|
||||
font-weight: 900;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.phaseBar {
|
||||
margin-top: 6px;
|
||||
height: 10px;
|
||||
background: #dfe8f0;
|
||||
border: 2px solid var(--line);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.phaseBar span {
|
||||
display: block;
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #ffd66e, #77c9ff, #6862e8);
|
||||
}
|
||||
|
||||
.clockHint {
|
||||
margin-top: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.edgeAdd {
|
||||
position: absolute;
|
||||
z-index: 8;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
width: 56px;
|
||||
height: 86px;
|
||||
transform: translateY(-50%);
|
||||
border: 3px solid var(--line);
|
||||
border-left: 0;
|
||||
border-radius: 0 18px 18px 0;
|
||||
background: var(--pink);
|
||||
color: white;
|
||||
font-size: 42px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
box-shadow: var(--hard-shadow);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edgeAdd:hover { background: var(--pink-dark); }
|
||||
.edgeAdd:active { transform: translateY(calc(-50% + 2px)); box-shadow: 2px 2px 0 rgba(34,45,63,.22); }
|
||||
|
||||
.toolHud {
|
||||
right: 14px;
|
||||
top: 116px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 7px;
|
||||
padding: 9px;
|
||||
}
|
||||
|
||||
.settingsPanel {
|
||||
top: 142px;
|
||||
right: 126px;
|
||||
z-index: 6;
|
||||
padding: 10px 12px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.iconButton,
|
||||
button,
|
||||
.ghostButton,
|
||||
.tab,
|
||||
.tool,
|
||||
.segmented button {
|
||||
border: 2px solid var(--line);
|
||||
border-radius: 0;
|
||||
background: #fffdf5;
|
||||
color: var(--ink);
|
||||
padding: 8px 11px;
|
||||
font-weight: 850;
|
||||
cursor: pointer;
|
||||
box-shadow: 3px 3px 0 rgba(36, 48, 68, .16);
|
||||
transition: transform .05s, box-shadow .05s, background .12s;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
.ghostButton:hover,
|
||||
.tab:hover,
|
||||
.tool:hover,
|
||||
.segmented button:hover { background: #fff1c8; }
|
||||
|
||||
button:active,
|
||||
.ghostButton:active,
|
||||
.tab:active,
|
||||
.tool:active,
|
||||
.segmented button:active {
|
||||
transform: translate(2px, 2px);
|
||||
box-shadow: 1px 1px 0 rgba(36, 48, 68, .16);
|
||||
}
|
||||
|
||||
button.active,
|
||||
.tab.active,
|
||||
.tool.active,
|
||||
.segmented button.active {
|
||||
background: var(--sun);
|
||||
box-shadow: inset 0 0 0 2px rgba(255,255,255,.4), 3px 3px 0 rgba(36,48,68,.18);
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background: var(--mint);
|
||||
color: #163a2a;
|
||||
}
|
||||
|
||||
button.secondary { background: var(--panel-2); }
|
||||
button.danger, .tool.danger, .iconButton.danger { background: #ffe4e4; color: #833232; }
|
||||
button.danger.active, .iconButton.danger.active { background: var(--danger); color: #fffdf5; }
|
||||
|
||||
.divider {
|
||||
height: 2px;
|
||||
background: var(--line);
|
||||
opacity: .22;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.infoHud {
|
||||
right: 14px;
|
||||
bottom: 14px;
|
||||
width: min(360px, calc(100vw - 28px));
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.selectedAsset {
|
||||
font-weight: 900;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px dashed rgba(45,60,80,.25);
|
||||
}
|
||||
|
||||
.tileInfo {
|
||||
white-space: pre-wrap;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.studioDrawer {
|
||||
position: absolute;
|
||||
z-index: 7;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: min(500px, calc(100vw - 20px));
|
||||
height: 100vh;
|
||||
background: var(--panel-solid);
|
||||
border-right: 3px solid var(--line);
|
||||
box-shadow: 10px 0 0 rgba(36, 48, 68, .12);
|
||||
transform: translateX(-104%);
|
||||
transition: transform .18s ease-out;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
}
|
||||
|
||||
.studioDrawer.open { transform: translateX(0); }
|
||||
|
||||
.drawerHead {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 10px 12px 8px;
|
||||
border-bottom: 3px solid var(--line);
|
||||
background: #ffe6f0;
|
||||
}
|
||||
|
||||
.drawerHead h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 950;
|
||||
}
|
||||
|
||||
.drawerHead p {
|
||||
margin: 3px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ghostButton {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
background: #fff2d0;
|
||||
border-bottom: 3px solid var(--line);
|
||||
}
|
||||
|
||||
.tab { padding: 8px 5px; font-size: 13px; }
|
||||
|
||||
.drawerBody {
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
background:
|
||||
linear-gradient(45deg, rgba(255,255,255,.4) 25%, transparent 25%) 0 0/18px 18px,
|
||||
var(--panel-solid);
|
||||
}
|
||||
|
||||
.tabPanel { display: none; }
|
||||
.tabPanel.active { display: block; }
|
||||
|
||||
.compactNameField { margin-bottom: 5px; }
|
||||
.roleHintInline { margin: 0 0 10px; }
|
||||
|
||||
.card {
|
||||
background: #fffdf5;
|
||||
border: 2px solid var(--line);
|
||||
box-shadow: 4px 4px 0 rgba(36,48,68,.12);
|
||||
padding: 9px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stack > * + * { margin-top: 10px; }
|
||||
.cardTitle {
|
||||
font-weight: 950;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
font-size: 12px;
|
||||
color: #3c4b60;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
border: 2px solid var(--line);
|
||||
border-radius: 0;
|
||||
background: #fffaf0;
|
||||
color: var(--ink);
|
||||
padding: 9px 10px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus, select:focus, textarea:focus { background: #fff; box-shadow: 0 0 0 3px rgba(119,201,255,.35); }
|
||||
textarea { resize: vertical; }
|
||||
input[type="color"] { height: 39px; padding: 3px; }
|
||||
input[type="checkbox"] { width: auto; margin: 0 8px 0 0; }
|
||||
|
||||
.twoCols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.segmented {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.segmented button { font-size: 12px; padding: 8px 5px; }
|
||||
|
||||
.hint {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.editorCard { padding-bottom: 10px; }
|
||||
.editorTop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
.sideSwitcher {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
.sideSwitcher button { padding: 6px 7px; font-size: 11px; }
|
||||
|
||||
.toolRow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(58px, 1fr));
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.editorActions {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
|
||||
.toolRow input { margin: 0; }
|
||||
.tool { padding: 7px 5px; font-size: 11px; }
|
||||
|
||||
#paintCanvas {
|
||||
display: block;
|
||||
width: min(100%, 330px);
|
||||
margin: 0 auto;
|
||||
aspect-ratio: 1 / 1;
|
||||
image-rendering: pixelated;
|
||||
border: 3px solid var(--line);
|
||||
background:
|
||||
linear-gradient(45deg, #f4ebdc 25%, transparent 25%) 0 0/16px 16px,
|
||||
linear-gradient(45deg, transparent 75%, #f4ebdc 75%) 0 0/16px 16px,
|
||||
#fffaf0;
|
||||
cursor: crosshair;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.compactSettings .twoCols { align-items: center; }
|
||||
.toggleLine {
|
||||
color: var(--ink);
|
||||
font-size: 13px;
|
||||
font-weight: 850;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.behaviorNote {
|
||||
border: 2px dashed rgba(45,60,80,.22);
|
||||
padding: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.actionRow {
|
||||
display: flex;
|
||||
gap: 9px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.actionRow.wrap { flex-wrap: wrap; }
|
||||
.actionRow button { flex: 1; }
|
||||
.lineageNote { min-height: 18px; color: var(--muted); font-size: 12px; }
|
||||
|
||||
.assetList { display: grid; gap: 10px; }
|
||||
.assetCard {
|
||||
display: grid;
|
||||
grid-template-columns: 68px 1fr;
|
||||
gap: 10px;
|
||||
border: 2px solid var(--line);
|
||||
background: #fff9e8;
|
||||
padding: 8px;
|
||||
box-shadow: 3px 3px 0 rgba(36,48,68,.12);
|
||||
}
|
||||
.assetCard.selected { background: #e6ffef; }
|
||||
.assetPreview {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
image-rendering: pixelated;
|
||||
background: #fff3d9;
|
||||
border: 2px solid var(--line);
|
||||
}
|
||||
.assetMeta strong { display: block; font-size: 14px; }
|
||||
.assetMeta span { display: block; margin-top: 3px; color: var(--muted); font-size: 12px; line-height: 1.3; }
|
||||
.assetActions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.assetActions button { padding: 5px 7px; font-size: 11px; }
|
||||
|
||||
.modeGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
.modeGrid button { padding: 8px 5px; font-size: 12px; }
|
||||
|
||||
.legend {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.legend span { display: flex; align-items: center; gap: 6px; }
|
||||
.legend i {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid var(--line);
|
||||
}
|
||||
.legend .water { background: #67b5d9; }
|
||||
.legend .sand { background: #ead493; }
|
||||
.legend .grass { background: #8bce76; }
|
||||
.legend .highland { background: #a7be80; }
|
||||
|
||||
#dataBox { min-height: 160px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; }
|
||||
|
||||
.toast {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
left: 50%;
|
||||
bottom: 22px;
|
||||
transform: translateX(-50%);
|
||||
background: #fffdf5;
|
||||
border: 3px solid var(--line);
|
||||
box-shadow: var(--hard-shadow);
|
||||
padding: 10px 14px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.topHud { align-items: flex-start; flex-direction: column; right: 70px; }
|
||||
.clockCard, .authorCard { width: 100%; }
|
||||
.toolHud { top: auto; bottom: 122px; right: 10px; }
|
||||
.infoHud { left: 10px; right: 10px; width: auto; }
|
||||
.studioDrawer { width: calc(100vw - 18px); }
|
||||
.toolRow { grid-template-columns: repeat(auto-fit, minmax(54px, 1fr)); }
|
||||
}
|
||||
|
||||
/* refinements */
|
||||
.toolHud {
|
||||
top: 142px;
|
||||
right: 18px;
|
||||
}
|
||||
.edgeAdd {
|
||||
top: calc(50% + 24px);
|
||||
width: 62px;
|
||||
height: 92px;
|
||||
background: linear-gradient(180deg, #ff97c2, #ff75aa);
|
||||
}
|
||||
.studioDrawer {
|
||||
width: min(520px, calc(100vw - 28px));
|
||||
}
|
||||
.drawerHead { background: #ffe1ef; }
|
||||
.drawerBody {
|
||||
background:
|
||||
linear-gradient(90deg, rgba(255,255,255,.42) 0 1px, transparent 1px) 0 0/20px 20px,
|
||||
linear-gradient(0deg, rgba(255,255,255,.42) 0 1px, transparent 1px) 0 0/20px 20px,
|
||||
#fff8e9;
|
||||
}
|
||||
.card {
|
||||
border-width: 3px;
|
||||
box-shadow: 5px 5px 0 rgba(36,48,68,.13);
|
||||
}
|
||||
.heroEditor {
|
||||
background: #fffdf7;
|
||||
border-color: #26364c;
|
||||
}
|
||||
#paintCanvas {
|
||||
margin-bottom: 10px;
|
||||
max-height: min(42vh, 330px);
|
||||
}
|
||||
.toolRow {
|
||||
grid-template-columns: repeat(auto-fit, minmax(58px, 1fr));
|
||||
margin-top: 0;
|
||||
}
|
||||
.paletteGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(13, 1fr);
|
||||
gap: 3px;
|
||||
margin: 6px 0 6px;
|
||||
padding: 6px;
|
||||
border: 2px solid rgba(36,48,68,.28);
|
||||
background: #fff6df;
|
||||
}
|
||||
.paletteSwatch {
|
||||
position: relative;
|
||||
aspect-ratio: 1 / 1;
|
||||
min-height: 18px;
|
||||
border: 2px solid rgba(36,48,68,.38);
|
||||
box-shadow: 2px 2px 0 rgba(36,48,68,.13);
|
||||
cursor: pointer;
|
||||
}
|
||||
.paletteSwatch.active {
|
||||
outline: 3px solid #ff7aad;
|
||||
outline-offset: 1px;
|
||||
border-color: #26364c;
|
||||
}
|
||||
.paletteSwatch::after {
|
||||
content: attr(data-code);
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
bottom: 0;
|
||||
color: rgba(20,26,38,.52);
|
||||
font-size: 7px;
|
||||
font-weight: 900;
|
||||
text-shadow: 0 1px 0 rgba(255,255,255,.7);
|
||||
}
|
||||
.quickSetupBar { margin-bottom: 6px; }
|
||||
.settingsCard .segmented { margin-top: 2px; }
|
||||
.markerRow {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
.markerChip {
|
||||
border: 2px dashed rgba(36,48,68,.25);
|
||||
background: #fff6df;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.summonCard {
|
||||
background: linear-gradient(180deg, #fffdf5, #fff2d7);
|
||||
}
|
||||
.finishActions {
|
||||
display: grid;
|
||||
grid-template-columns: 1.35fr .8fr .6fr;
|
||||
}
|
||||
.bigPrimary {
|
||||
min-height: 48px;
|
||||
font-size: 14px;
|
||||
background: linear-gradient(180deg, #89e4ae, #65d596) !important;
|
||||
}
|
||||
.sideSwitcher button:first-child.active::after,
|
||||
.sideSwitcher button:nth-child(2).active::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
}
|
||||
.spawnPop {
|
||||
pointer-events: none;
|
||||
}
|
||||
.selectedAsset { font-weight: 900; padding-bottom: 8px; border-bottom: 2px dashed rgba(45,60,80,.25); }
|
||||
@media (max-width: 720px) {
|
||||
.toolHud { top: auto; bottom: 126px; right: 10px; }
|
||||
.finishActions { grid-template-columns: 1fr; }
|
||||
.paletteGrid { grid-template-columns: repeat(8, 1fr); }
|
||||
}
|
||||
|
||||
.paletteGrid button { width: 100%; aspect-ratio: 1 / 1; padding: 0; min-height: 22px; }
|
||||
.sideSwitcher button { white-space: nowrap; }
|
||||
.heroEditor .hint { line-height: 1.35; }
|
||||
|
||||
|
||||
.compactEditorTop { margin-bottom: 4px; min-height: 0; }
|
||||
body, button, input, select, textarea { font-smooth: never; -webkit-font-smoothing: none; }
|
||||
|
||||
/* latest layout fixes */
|
||||
.studioDrawer { width: min(600px, calc(100vw - 24px)); }
|
||||
.paintLayout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 360px) 1fr;
|
||||
align-items: start;
|
||||
gap: 10px;
|
||||
margin: 4px 0 8px;
|
||||
}
|
||||
#paintCanvas {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
max-height: none;
|
||||
margin: 0;
|
||||
}
|
||||
.paletteGrid {
|
||||
margin: 0;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
align-content: start;
|
||||
gap: 4px;
|
||||
max-height: 360px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.paletteSwatch, .paletteGrid button {
|
||||
min-height: 20px;
|
||||
}
|
||||
.sideSwitcher { justify-content: flex-start; }
|
||||
.authorCard span { white-space: nowrap; }
|
||||
@media (max-width: 720px) {
|
||||
.paintLayout { grid-template-columns: 1fr; }
|
||||
.paletteGrid { grid-template-columns: repeat(10, 1fr); max-height: none; }
|
||||
}
|
||||
|
||||
/* Selection bubble + outline controls */
|
||||
.palettePanel { display: grid; gap: 8px; }
|
||||
.outlineControls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
font-size: 10px;
|
||||
}
|
||||
.outlineControls span {
|
||||
grid-column: 1 / -1;
|
||||
display: block;
|
||||
border: 2px solid rgba(36,48,68,.28);
|
||||
background: #fffaf0;
|
||||
padding: 5px 6px;
|
||||
font-weight: 900;
|
||||
}
|
||||
.selectionBubble {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 9;
|
||||
min-width: 126px;
|
||||
max-width: 210px;
|
||||
padding: 8px;
|
||||
background: #fffdf5;
|
||||
border: 3px solid var(--line);
|
||||
box-shadow: 4px 4px 0 rgba(36,48,68,.20);
|
||||
pointer-events: auto;
|
||||
text-align: center;
|
||||
}
|
||||
.selectionBubble::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -10px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #fffdf5;
|
||||
border-right: 3px solid var(--line);
|
||||
border-bottom: 3px solid var(--line);
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
.bubbleName {
|
||||
font-size: 12px;
|
||||
font-weight: 950;
|
||||
line-height: 1.2;
|
||||
word-break: break-word;
|
||||
}
|
||||
.bubbleAuthor {
|
||||
margin-top: 2px;
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
font-weight: 800;
|
||||
}
|
||||
.bubbleVotes {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.bubbleVotes button {
|
||||
padding: 3px 7px;
|
||||
min-width: 30px;
|
||||
font-size: 11px;
|
||||
}
|
||||
#voteScore {
|
||||
font-weight: 950;
|
||||
font-size: 12px;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.bubbleRemixFrom, .bubbleRemixCount {
|
||||
margin-top: 3px;
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.bubbleActions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 5px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.bubbleActions button {
|
||||
padding: 4px 6px;
|
||||
font-size: 10px;
|
||||
}
|
||||
.bubbleVotes button.mutedVote, .assetActions button.mutedAction {
|
||||
opacity: .45;
|
||||
}
|
||||
.bubbleVotes button.activeVote {
|
||||
background: var(--sun);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue