shoo the puppets to the ground (ugly ahh graphics lmao)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE scene_configuration
|
||||
ADD COLUMN puppet_movement_mode TEXT NOT NULL DEFAULT 'free'
|
||||
CHECK (puppet_movement_mode IN ('free', 'bottom'));
|
||||
@@ -20,6 +20,7 @@ use tauri::Manager;
|
||||
async fn launch_app(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let handle = app.handle();
|
||||
db::init(handle).await?;
|
||||
scene_configuration::init(handle).await?;
|
||||
keypair::init(handle).await?;
|
||||
app.manage(interactions::InteractionState::default());
|
||||
network::init(handle).await?;
|
||||
|
||||
@@ -11,10 +11,12 @@ use tauri_specta::Event;
|
||||
use tokio::time::MissedTickBehavior;
|
||||
|
||||
use crate::cursor::{CursorPosition, CursorPositions, CursorState};
|
||||
use crate::scene_configuration::{PuppetMovementMode, SceneConfigurationState};
|
||||
|
||||
const TICK_INTERVAL: Duration = Duration::from_millis(125);
|
||||
const SPEED_LOGICAL_PIXELS_PER_SECOND: f64 = 80.0;
|
||||
const FOLLOW_RADIUS_LOGICAL_PIXELS: f64 = 48.0;
|
||||
const BOTTOM_MOVEMENT_Y: f64 = 1.0;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -39,9 +41,16 @@ impl PuppetStateStore {
|
||||
cursor_positions: &HashMap<String, CursorPositions>,
|
||||
viewport: Viewport,
|
||||
elapsed: Duration,
|
||||
movement_mode: PuppetMovementMode,
|
||||
) -> Result<Option<Vec<PuppetState>>, String> {
|
||||
let mut puppets = self.0.write().map_err(|error| error.to_string())?;
|
||||
let changed = advance_puppets(&mut puppets, cursor_positions, viewport, elapsed);
|
||||
let changed = advance_puppets(
|
||||
&mut puppets,
|
||||
cursor_positions,
|
||||
viewport,
|
||||
elapsed,
|
||||
movement_mode,
|
||||
);
|
||||
Ok(changed.then(|| sorted_snapshot(&puppets)))
|
||||
}
|
||||
|
||||
@@ -95,10 +104,21 @@ pub fn init(handle: &AppHandle) -> Result<(), String> {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let movement_mode = match handle
|
||||
.state::<SceneConfigurationState>()
|
||||
.puppet_movement_mode()
|
||||
{
|
||||
Ok(mode) => mode,
|
||||
Err(error) => {
|
||||
eprintln!("failed to read Puppet movement mode: {error}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let puppets = match handle.state::<PuppetStateStore>().update(
|
||||
&cursor_positions,
|
||||
viewport,
|
||||
elapsed,
|
||||
movement_mode,
|
||||
) {
|
||||
Ok(Some(puppets)) => puppets,
|
||||
Ok(None) => continue,
|
||||
@@ -128,18 +148,20 @@ fn advance_puppets(
|
||||
cursor_positions: &HashMap<String, CursorPositions>,
|
||||
viewport: Viewport,
|
||||
elapsed: Duration,
|
||||
movement_mode: PuppetMovementMode,
|
||||
) -> bool {
|
||||
let previous_len = puppets.len();
|
||||
puppets.retain(|id, _| cursor_positions.contains_key(id));
|
||||
let mut changed = puppets.len() != previous_len;
|
||||
|
||||
for (id, cursor) in cursor_positions {
|
||||
let target = movement_target(&cursor.mapped, movement_mode);
|
||||
let Some(puppet) = puppets.get_mut(id) else {
|
||||
puppets.insert(
|
||||
id.clone(),
|
||||
PuppetState {
|
||||
id: id.clone(),
|
||||
position: cursor.mapped.clone(),
|
||||
position: target,
|
||||
is_moving: false,
|
||||
},
|
||||
);
|
||||
@@ -147,12 +169,26 @@ fn advance_puppets(
|
||||
continue;
|
||||
};
|
||||
|
||||
changed |= advance_puppet(puppet, &cursor.mapped, viewport, elapsed.as_secs_f64());
|
||||
if movement_mode == PuppetMovementMode::Bottom && puppet.position.y != BOTTOM_MOVEMENT_Y {
|
||||
puppet.position.y = BOTTOM_MOVEMENT_Y;
|
||||
changed = true;
|
||||
}
|
||||
changed |= advance_puppet(puppet, &target, viewport, elapsed.as_secs_f64());
|
||||
}
|
||||
|
||||
changed
|
||||
}
|
||||
|
||||
fn movement_target(cursor: &CursorPosition, mode: PuppetMovementMode) -> CursorPosition {
|
||||
CursorPosition {
|
||||
x: cursor.x,
|
||||
y: match mode {
|
||||
PuppetMovementMode::Free => cursor.y,
|
||||
PuppetMovementMode::Bottom => BOTTOM_MOVEMENT_Y,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn advance_puppet(
|
||||
puppet: &mut PuppetState,
|
||||
target: &CursorPosition,
|
||||
@@ -220,6 +256,7 @@ mod tests {
|
||||
&cursors(&[("friend", 0.75, 0.25)]),
|
||||
viewport(),
|
||||
TICK_INTERVAL,
|
||||
PuppetMovementMode::Free,
|
||||
);
|
||||
|
||||
assert!(changed);
|
||||
@@ -296,10 +333,46 @@ mod tests {
|
||||
&HashMap::new(),
|
||||
viewport(),
|
||||
TICK_INTERVAL,
|
||||
PuppetMovementMode::Free,
|
||||
));
|
||||
assert!(puppets.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bottom_mode_preserves_horizontal_target_and_insets_vertical_target() {
|
||||
let cursor = CursorPosition { x: 0.25, y: 0.1 };
|
||||
|
||||
assert_eq!(
|
||||
movement_target(&cursor, PuppetMovementMode::Bottom),
|
||||
CursorPosition {
|
||||
x: 0.25,
|
||||
y: BOTTOM_MOVEMENT_Y,
|
||||
}
|
||||
);
|
||||
assert_eq!(movement_target(&cursor, PuppetMovementMode::Free), cursor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bottom_mode_emits_a_fixed_inset_y_for_existing_puppets() {
|
||||
let mut puppets = HashMap::from([(
|
||||
"friend".to_owned(),
|
||||
PuppetState {
|
||||
id: "friend".to_owned(),
|
||||
position: CursorPosition { x: 0.25, y: 0.1 },
|
||||
is_moving: false,
|
||||
},
|
||||
)]);
|
||||
|
||||
assert!(advance_puppets(
|
||||
&mut puppets,
|
||||
&cursors(&[("friend", 0.25, 0.1)]),
|
||||
viewport(),
|
||||
TICK_INTERVAL,
|
||||
PuppetMovementMode::Bottom,
|
||||
));
|
||||
assert_eq!(puppets["friend"].position.y, BOTTOM_MOVEMENT_Y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalized_step_accounts_for_viewport_aspect_ratio() {
|
||||
let mut horizontal = PuppetState {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::db::{self, AppDatabase};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use tauri::{AppHandle, State};
|
||||
use std::sync::RwLock;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tauri_specta::Event;
|
||||
|
||||
const SCENE_CONFIGURATION_ID: i64 = 1;
|
||||
@@ -10,11 +11,41 @@ const MAX_PUPPET_SCALE: f64 = 2.0;
|
||||
const MIN_PUPPET_OPACITY: f64 = 0.1;
|
||||
const MAX_PUPPET_OPACITY: f64 = 1.0;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, Type, sqlx::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sqlx(type_name = "TEXT", rename_all = "lowercase")]
|
||||
pub enum PuppetMovementMode {
|
||||
#[default]
|
||||
Free,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Type, sqlx::FromRow)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SceneConfiguration {
|
||||
pub puppet_scale: f64,
|
||||
pub puppet_opacity: f64,
|
||||
pub puppet_movement_mode: PuppetMovementMode,
|
||||
}
|
||||
|
||||
pub struct SceneConfigurationState(RwLock<SceneConfiguration>);
|
||||
|
||||
impl SceneConfigurationState {
|
||||
fn new(configuration: SceneConfiguration) -> Self {
|
||||
Self(RwLock::new(configuration))
|
||||
}
|
||||
|
||||
fn replace(&self, configuration: SceneConfiguration) -> Result<(), String> {
|
||||
*self.0.write().map_err(|error| error.to_string())? = configuration;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn puppet_movement_mode(&self) -> Result<PuppetMovementMode, String> {
|
||||
self.0
|
||||
.read()
|
||||
.map(|configuration| configuration.puppet_movement_mode)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||
@@ -25,7 +56,7 @@ pub struct SceneConfigurationChanged {
|
||||
|
||||
async fn get(database: &AppDatabase) -> Result<SceneConfiguration, sqlx::Error> {
|
||||
sqlx::query_as::<_, SceneConfiguration>(
|
||||
"SELECT puppet_scale, puppet_opacity FROM scene_configuration WHERE id = ?1",
|
||||
"SELECT puppet_scale, puppet_opacity, puppet_movement_mode FROM scene_configuration WHERE id = ?1",
|
||||
)
|
||||
.bind(SCENE_CONFIGURATION_ID)
|
||||
.fetch_one(database.pool())
|
||||
@@ -52,10 +83,11 @@ async fn update(
|
||||
}
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE scene_configuration SET puppet_scale = ?1, puppet_opacity = ?2 WHERE id = ?3",
|
||||
"UPDATE scene_configuration SET puppet_scale = ?1, puppet_opacity = ?2, puppet_movement_mode = ?3 WHERE id = ?4",
|
||||
)
|
||||
.bind(configuration.puppet_scale)
|
||||
.bind(configuration.puppet_opacity)
|
||||
.bind(configuration.puppet_movement_mode)
|
||||
.bind(SCENE_CONFIGURATION_ID)
|
||||
.execute(database.pool())
|
||||
.await
|
||||
@@ -64,6 +96,12 @@ async fn update(
|
||||
get(database).await.map_err(db::command_error)
|
||||
}
|
||||
|
||||
pub async fn init(handle: &AppHandle) -> Result<(), sqlx::Error> {
|
||||
let configuration = get(&handle.state::<AppDatabase>()).await?;
|
||||
handle.manage(SceneConfigurationState::new(configuration));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_changed(handle: &AppHandle, configuration: SceneConfiguration) -> Result<(), String> {
|
||||
SceneConfigurationChanged { configuration }
|
||||
.emit(handle)
|
||||
@@ -75,8 +113,10 @@ fn emit_changed(handle: &AppHandle, configuration: SceneConfiguration) -> Result
|
||||
pub async fn get_scene_configuration(
|
||||
handle: AppHandle,
|
||||
database: State<'_, AppDatabase>,
|
||||
state: State<'_, SceneConfigurationState>,
|
||||
) -> Result<SceneConfiguration, String> {
|
||||
let configuration = get(&database).await.map_err(db::command_error)?;
|
||||
state.replace(configuration.clone())?;
|
||||
emit_changed(&handle, configuration.clone())?;
|
||||
Ok(configuration)
|
||||
}
|
||||
@@ -86,9 +126,11 @@ pub async fn get_scene_configuration(
|
||||
pub async fn update_scene_configuration(
|
||||
handle: AppHandle,
|
||||
database: State<'_, AppDatabase>,
|
||||
state: State<'_, SceneConfigurationState>,
|
||||
configuration: SceneConfiguration,
|
||||
) -> Result<SceneConfiguration, String> {
|
||||
let configuration = update(&database, configuration).await?;
|
||||
state.replace(configuration.clone())?;
|
||||
emit_changed(&handle, configuration.clone())?;
|
||||
Ok(configuration)
|
||||
}
|
||||
@@ -121,6 +163,7 @@ mod tests {
|
||||
SceneConfiguration {
|
||||
puppet_scale: 1.0,
|
||||
puppet_opacity: 1.0,
|
||||
puppet_movement_mode: PuppetMovementMode::Free,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -134,12 +177,17 @@ mod tests {
|
||||
SceneConfiguration {
|
||||
puppet_scale: 1.5,
|
||||
puppet_opacity: 0.4,
|
||||
puppet_movement_mode: PuppetMovementMode::Bottom,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("update scene configuration");
|
||||
assert_eq!(configuration.puppet_scale, 1.5);
|
||||
assert_eq!(configuration.puppet_opacity, 0.4);
|
||||
assert_eq!(
|
||||
configuration.puppet_movement_mode,
|
||||
PuppetMovementMode::Bottom
|
||||
);
|
||||
assert_eq!(get(&database).await.unwrap(), configuration);
|
||||
|
||||
let error = update(
|
||||
@@ -147,6 +195,7 @@ mod tests {
|
||||
SceneConfiguration {
|
||||
puppet_scale: 2.1,
|
||||
puppet_opacity: 0.4,
|
||||
puppet_movement_mode: PuppetMovementMode::Bottom,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -159,6 +208,7 @@ mod tests {
|
||||
SceneConfiguration {
|
||||
puppet_scale: 1.5,
|
||||
puppet_opacity: 0.05,
|
||||
puppet_movement_mode: PuppetMovementMode::Bottom,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
||||
+2
-1
@@ -144,12 +144,13 @@ export type FriendsChanged = { friends: Friend[] }
|
||||
export type InteractionContent = { type: "text"; text: string } | { type: "wave" } | { type: "image"; mediaType: string; data: string }
|
||||
export type NetworkStatusChanged = { statuses: ConnectionStatus[] }
|
||||
export type ProfileChanged = { profile: User }
|
||||
export type PuppetMovementMode = "free" | "bottom"
|
||||
export type PuppetState = { id: string; position: CursorPosition; isMoving: boolean }
|
||||
export type PuppetStatesChanged = { puppets: PuppetState[] }
|
||||
export type Remote = { id: string; address: string; name: string | null; port: number | null }
|
||||
export type RemoteInput = { address: string; name: string | null; port: number | null }
|
||||
export type RemotesChanged = { remotes: Remote[] }
|
||||
export type SceneConfiguration = { puppetScale: number; puppetOpacity: number }
|
||||
export type SceneConfiguration = { puppetScale: number; puppetOpacity: number; puppetMovementMode: PuppetMovementMode }
|
||||
export type SceneConfigurationChanged = { configuration: SceneConfiguration }
|
||||
export type SceneHitbox = { x: number; y: number; width: number; height: number }
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { commands } from "$lib/bindings";
|
||||
import { commands, type PuppetMovementMode } from "$lib/bindings";
|
||||
import {
|
||||
sceneConfiguration,
|
||||
sceneConfigurationListenerError,
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
let puppetScale = $state(1);
|
||||
let puppetOpacity = $state(1);
|
||||
let puppetMovementMode = $state<PuppetMovementMode>("free");
|
||||
let dirty = $state(false);
|
||||
let busy = $state(false);
|
||||
let error = $state("");
|
||||
@@ -20,6 +21,7 @@
|
||||
if (!dirty) {
|
||||
puppetScale = $sceneConfiguration.puppetScale;
|
||||
puppetOpacity = $sceneConfiguration.puppetOpacity;
|
||||
puppetMovementMode = $sceneConfiguration.puppetMovementMode;
|
||||
}
|
||||
register("scene", { apply, reset }, { dirty, busy });
|
||||
});
|
||||
@@ -29,13 +31,15 @@
|
||||
function updateDirty() {
|
||||
dirty =
|
||||
puppetScale !== $sceneConfiguration.puppetScale ||
|
||||
puppetOpacity !== $sceneConfiguration.puppetOpacity;
|
||||
puppetOpacity !== $sceneConfiguration.puppetOpacity ||
|
||||
puppetMovementMode !== $sceneConfiguration.puppetMovementMode;
|
||||
error = "";
|
||||
}
|
||||
|
||||
function reset() {
|
||||
puppetScale = $sceneConfiguration.puppetScale;
|
||||
puppetOpacity = $sceneConfiguration.puppetOpacity;
|
||||
puppetMovementMode = $sceneConfiguration.puppetMovementMode;
|
||||
dirty = false;
|
||||
error = "";
|
||||
}
|
||||
@@ -49,9 +53,11 @@
|
||||
const configuration = await commands.updateSceneConfiguration({
|
||||
puppetScale,
|
||||
puppetOpacity,
|
||||
puppetMovementMode,
|
||||
});
|
||||
puppetScale = configuration.puppetScale;
|
||||
puppetOpacity = configuration.puppetOpacity;
|
||||
puppetMovementMode = configuration.puppetMovementMode;
|
||||
dirty = false;
|
||||
return true;
|
||||
} catch (cause) {
|
||||
@@ -110,5 +116,72 @@
|
||||
disabled={busy}
|
||||
aria-describedby="puppet-opacity-help"
|
||||
/>
|
||||
|
||||
<fieldset class="mt-4">
|
||||
<legend class="fieldset-label mb-2">Movement</legend>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label
|
||||
class:border-primary={puppetMovementMode === "free"}
|
||||
class="card cursor-pointer overflow-hidden border border-base-300 bg-base-200"
|
||||
>
|
||||
<img
|
||||
src="/puppet-movement-free.svg"
|
||||
alt="Puppets moving freely across the full viewport"
|
||||
class="w-full object-cover bg-linear-to-b from-base-100 to-base-300"
|
||||
class:from-primary-content={puppetMovementMode === "free"}
|
||||
/>
|
||||
<span
|
||||
class="card-body items-center gap-2 p-2 text-center"
|
||||
class:bg-primary-content={puppetMovementMode === "free"}
|
||||
class:text-primary={puppetMovementMode === "free"}
|
||||
>
|
||||
<span class="text-xs font-medium">Freeroam around</span>
|
||||
<input
|
||||
class="hidden"
|
||||
type="radio"
|
||||
name="puppet-movement-mode"
|
||||
value="free"
|
||||
checked={puppetMovementMode === "free"}
|
||||
onchange={() => {
|
||||
puppetMovementMode = "free";
|
||||
updateDirty();
|
||||
}}
|
||||
disabled={busy}
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label
|
||||
class:border-primary={puppetMovementMode === "bottom"}
|
||||
class:text-primary={puppetMovementMode === "bottom"}
|
||||
class="card cursor-pointer overflow-hidden border border-base-300 bg-base-200"
|
||||
>
|
||||
<img
|
||||
src="/puppet-movement-bottom.svg"
|
||||
alt="Puppets moving along the bottom of the viewport"
|
||||
class="w-full object-cover bg-linear-to-b from-base-100 to-base-300"
|
||||
class:from-primary-content={puppetMovementMode === "bottom"}
|
||||
/>
|
||||
<span
|
||||
class="card-body items-center gap-2 p-2 text-center"
|
||||
class:bg-primary-content={puppetMovementMode === "bottom"}
|
||||
>
|
||||
<span class="text-xs font-medium">Stay on the ground</span>
|
||||
<input
|
||||
class="hidden"
|
||||
type="radio"
|
||||
name="puppet-movement-mode"
|
||||
value="bottom"
|
||||
checked={puppetMovementMode === "bottom"}
|
||||
onchange={() => {
|
||||
puppetMovementMode = "bottom";
|
||||
updateDirty();
|
||||
}}
|
||||
disabled={busy}
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
export const sceneConfiguration = writable<SceneConfiguration>({
|
||||
puppetScale: 1,
|
||||
puppetOpacity: 1,
|
||||
puppetMovementMode: "free",
|
||||
});
|
||||
export const sceneConfigurationListenerError = writable("");
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<svg width="200" height="150" viewBox="0 0 200 150" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="60.5" y="10.5" width="115" height="80" fill="#AAAAAA" stroke="#BBBBBB"/>
|
||||
<rect opacity="0.5" x="64" y="30" width="108" height="57" fill="#DDDDDD"/>
|
||||
<rect x="67" y="17" width="7" height="7" fill="#DDDDDD"/>
|
||||
<rect x="77" y="17" width="7" height="7" fill="#DDDDDD"/>
|
||||
<rect x="87" y="17" width="7" height="7" fill="#DDDDDD"/>
|
||||
<rect x="23.5" y="35.5" width="115" height="80" fill="#AAAAAA" stroke="#BBBBBB"/>
|
||||
<rect opacity="0.5" x="27" y="55" width="108" height="57" fill="#DDDDDD"/>
|
||||
<rect x="30" y="42" width="7" height="7" fill="#DDDDDD"/>
|
||||
<rect x="40" y="42" width="7" height="7" fill="#DDDDDD"/>
|
||||
<rect x="50" y="42" width="7" height="7" fill="#DDDDDD"/>
|
||||
<rect x="52.6701" y="137" width="5" height="12" transform="rotate(8 52.6701 137)" fill="white"/>
|
||||
<rect x="60" y="137.696" width="5" height="12" transform="rotate(-8 60 137.696)" fill="white"/>
|
||||
<rect x="48.1042" y="125.507" width="5" height="12" transform="rotate(20 48.1042 125.507)" fill="white"/>
|
||||
<rect x="65" y="127.217" width="5" height="12" transform="rotate(-20 65 127.217)" fill="white"/>
|
||||
<rect x="51" y="124" width="16" height="16" fill="#EEEEEE"/>
|
||||
<rect x="45" y="96" width="28" height="28" fill="white"/>
|
||||
<rect x="89.6701" y="137" width="5" height="12" transform="rotate(8 89.6701 137)" fill="white"/>
|
||||
<rect x="97" y="137.696" width="5" height="12" transform="rotate(-8 97 137.696)" fill="white"/>
|
||||
<rect x="85.1042" y="125.507" width="5" height="12" transform="rotate(20 85.1042 125.507)" fill="white"/>
|
||||
<rect x="102" y="127.217" width="5" height="12" transform="rotate(-20 102 127.217)" fill="white"/>
|
||||
<rect x="88" y="124" width="16" height="16" fill="#EEEEEE"/>
|
||||
<rect x="82" y="96" width="28" height="28" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg width="200" height="150" viewBox="0 0 200 150" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="60.5" y="10.5" width="115" height="80" fill="#AAAAAA" stroke="#BBBBBB"/>
|
||||
<rect opacity="0.5" x="64" y="30" width="108" height="57" fill="#DDDDDD"/>
|
||||
<rect x="67" y="17" width="7" height="7" fill="#DDDDDD"/>
|
||||
<rect x="77" y="17" width="7" height="7" fill="#DDDDDD"/>
|
||||
<rect x="87" y="17" width="7" height="7" fill="#DDDDDD"/>
|
||||
<rect x="23.5" y="35.5" width="115" height="80" fill="#AAAAAA" stroke="#BBBBBB"/>
|
||||
<rect opacity="0.5" x="27" y="55" width="108" height="57" fill="#DDDDDD"/>
|
||||
<rect x="30" y="42" width="7" height="7" fill="#DDDDDD"/>
|
||||
<rect x="40" y="42" width="7" height="7" fill="#DDDDDD"/>
|
||||
<rect x="50" y="42" width="7" height="7" fill="#DDDDDD"/>
|
||||
<rect x="134.67" y="115" width="5" height="12" transform="rotate(8 134.67 115)" fill="white"/>
|
||||
<rect x="142" y="115.696" width="5" height="12" transform="rotate(-8 142 115.696)" fill="white"/>
|
||||
<rect x="130.104" y="103.507" width="5" height="12" transform="rotate(20 130.104 103.507)" fill="white"/>
|
||||
<rect x="147" y="105.217" width="5" height="12" transform="rotate(-20 147 105.217)" fill="white"/>
|
||||
<rect x="133" y="102" width="16" height="16" fill="#EEEEEE"/>
|
||||
<rect x="127" y="74" width="28" height="28" fill="white"/>
|
||||
<rect x="52.6701" y="63" width="5" height="12" transform="rotate(8 52.6701 63)" fill="white"/>
|
||||
<rect x="60" y="63.6959" width="5" height="12" transform="rotate(-8 60 63.6959)" fill="white"/>
|
||||
<rect x="48.1042" y="51.5068" width="5" height="12" transform="rotate(20 48.1042 51.5068)" fill="white"/>
|
||||
<rect x="65" y="53.2169" width="5" height="12" transform="rotate(-20 65 53.2169)" fill="white"/>
|
||||
<rect x="51" y="50" width="16" height="16" fill="#EEEEEE"/>
|
||||
<rect x="45" y="22" width="28" height="28" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
Reference in New Issue
Block a user