読み方
「そりちゅーど・ぱーてぃくるず」といいます。
図書館の自習室のような、「誰か同じことをして頑張っている人がいる」のを感じられるようなサービスが欲しくて作りました。
イメージ・コンセプトは「スノードーム」です。
個人開発中。口コミ歓迎。リンクフリー。
▼ 基本的な使い方
行う作業を選択してStartボタンを押してください。
作業終了するときはEndボタンを押します。
▼ 背景設定 / Background Customization
「⚙️」アイコン:背景設定できます(詳しい説明)- お好みの背景画像に変更することができます。
- 設定した画像はブラウザに保存されます。サーバにはアップロードされません。
▼ メモ覧 / Memo Function
- いったん機能削除済みです。
▼ 「白いパーティクルは?」/ What is the white particles?
- ダミーパーティクルです。「妖精さん」と呼んでいます。
▼「End」ボタンを押していないのに作業終了した状態になっている件について
「あれ?意図して【End】ボタンを押した記憶はないのに、押したことになっている?」
というケースに直面されたユーザーさんもいらっしゃるかもしれません。
これは以下の2点が原因です。①長時間経過した際には一度強制的にセッションを終わらせる」仕様にしているため
②ホストしているウェブ側で不具合が起きている可能性詳細はこちらの記事をご覧ください:2025_W08のアップデート
▼ 3D View
- いったん機能削除済みです。
個人サイトでの利用
WebAPI/エンドポイントを公開しています。
こちらの記事に詳細を記載していますので、よろしければご参照ください。
(記事URL: https://note.com/sopars/n/n56f41faf418d)また、IFRAMEを利用することで、個人サイトのウィジェットとして利用できるようにもしています。
お手持ちの個人サイトで利用する場合はIFRAMEにURLを埋め込んでください。(利用例)
IFRAMEの実装の仕方
今はもうChatGPTやGemini等のAIに質問するとソースコードごと書いてくれると思います
sandbox属性など利用しつつ作ってみてくださいご利用いただく場合、特にリンクなど貼らないでいただいて大丈夫です
(タイトルにURLが貼られていることもあり)
その他仕様(今後、変更される可能性があります)
- リアルタイム性はあまり追求していません。
- 更新にタイムラグがあることがあります。
Activityについて
選択可能な作業(Activity)はユーザーの皆様のご利用状況とご要望の様子を見て追加していきます。
モバイル対応
アプリはありませんが通常のブラウザでご利用いただけます。
お知らせなど
- note:週次で開発ログを公開しています。
- X:Xでの広報アカウントです(SNSに関する考えメモ)。
Powered by
Carrd / COMPASS LINK / note / GitHub / Replit / X / Solomaker
▼ ウィジェットのサンプル
- お手持ちの個人サイトなどに以下のJavaScriptを埋め込んでご利用いただくことができます。
- 透過処理をしているので、背景になじむようになっています。
- お好みのデザインに改変してご利用ください。
<div id="sp-globe-widget" style="
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
font-family: system-ui, -apple-system, sans-serif;
color: currentColor;
">
<!-- スノーグローブ -->
<div id="sp-globe" style="
position: relative;
width: 120px;
height: 120px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, rgba(255,255,255,0.12) 0%, rgba(255,255,255,0.03) 50%, transparent 70%);
border: 1px solid rgba(128,128,128,0.2);
box-shadow: inset 0 2px 8px rgba(255,255,255,0.08), 0 4px 16px rgba(0,0,0,0.1);
overflow: hidden;
">
<canvas id="sp-canvas" width="120" height="120" style="width:100%;height:100%;"></canvas>
</div>
<!-- SolParSリンク -->
<a href="https://solpars.work/" target="blank" rel="noopener noreferrer" style="
font-size: 0.65rem;
opacity: 0.4;
color: currentColor;
text-decoration: none;
letter-spacing: 0.08em;
transition: opacity 0.3s;
" onmouseover="this.style.opacity='0.7'" onmouseout="this.style.opacity='0.4'">Solitude ParticleS</a>
<!-- ラベル -->
<div id="sp-label" style="
font-size: 0.8rem;
opacity: 0.7;
text-align: center;
line-height: 1.4;
"></div>
</div><script>
(function() {
const APIURL = 'https://solpars.work/api/sessions/counts';
const POLLINTERVAL = 10000;// アクティビティごとの色(SolParS本家と同じ配色)
const ACTIVITYCOLORS = {
'読書中 / Reading': '#818cf8',
'コーディング中 / Programming': '#34d399',
'GE作業中(UE, Unity, Godot,etc)': '#60a5fa',
'DCCツール作業中(Blender, Houdini,etc)': '#fbbf24',
'執筆中 / Writing': '#f472b6',
'勉強中 / Studying': '#a78bfa',
'仕事中 / Working': '#c1f7f7',
'イラスト作成中 / Drawing': '#ec4899',
'部屋片づけ中 / Cleaning': '#84cc16',
'その他 / Other': '#f5e50a'
};const canvas = document.getElementById('sp-canvas');
const ctx = canvas.getContext('2d');
const label = document.getElementById('sp-label');const CX = 60, CY = 60, RADIUS = 50;
let particles = [];
let dummies = [];
let animId = null;// --- パーティクルクラス ---
class Particle {
constructor(color, isDummy) {
this.isDummy = isDummy;
this.color = color;
this.r = isDummy ? 2 : 3.5;
this.opacity = isDummy ? 0.4 : 0.8;
// 円形内のランダム位置
const angle = Math.random() * Math.PI * 2;
const dist = Math.sqrt(Math.random()) * (RADIUS - this.r - 2);
this.x = CX + dist * Math.cos(angle);
this.y = CY + dist * Math.sin(angle);
this.vx = (Math.random() - 0.5) * 0.4;
this.vy = (Math.random() - 0.5) * 0.4;
}update() {
this.vx += (Math.random() - 0.5) * 0.02;
this.vy += (Math.random() - 0.5) * 0.02;
const maxSpd = this.isDummy ? 0.2 : 0.3;
this.vx = Math.max(-maxSpd, Math.min(maxSpd, this.vx)) * 0.99;
this.vy = Math.max(-maxSpd, Math.min(maxSpd, this.vy)) * 0.99;
let nx = this.x + this.vx;
let ny = this.y + this.vy;
const dx = nx - CX, dy = ny - CY;
const dist = Math.sqrt(dx * dx + dy * dy);
const maxDist = RADIUS - this.r - 2;
if (dist > maxDist) {
const scale = maxDist / dist;
nx = CX + dx * scale;
ny = CY + dy * scale;
this.vx *= -0.5;
this.vy *= -0.5;
}
this.x = nx;
this.y = ny;
}draw(ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.globalAlpha = this.opacity;
ctx.fill();
// グロー
if (!this.isDummy) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.r + 3, 0, Math.PI * 2);
const grad = ctx.createRadialGradient(this.x, this.y, this.r, this.x, this.y, this.r + 3);
grad.addColorStop(0, this.color);
grad.addColorStop(1, 'transparent');
ctx.fillStyle = grad;
ctx.globalAlpha = 0.3;
ctx.fill();
}
ctx.globalAlpha = 1;
}
}// --- ダミーパーティクル初期化 ---
function initDummies() {
dummies = [];
const count = 2 + Math.floor(Math.random() * 3);
for (let i = 0; i < count; i++) {
dummies.push(new Particle('#ffffff', true));
}
}// --- データ取得 → パーティクル更新 ---
async function fetchAndUpdate() {
try {
const res = await fetch(APIURL);
const data = await res.json();
const entries = Object.values(data.counts);
const total = entries.reduce((s, item) => s + item.count, 0);// パーティクルを再構築
const newParticles = [];
entries.forEach(item => {
const color = ACTIVITYCOLORS[item.name] || '#94a3b8';
for (let i = 0; i < item.count; i++) {
newParticles.push(new Particle(color, false));
}
});
particles = newParticles;// ラベル更新
if (total === 0) {
label.textContent = '☕ みんなお休み中...';
} else {
const names = entries.map(item => {
const shortName = item.name.split(' / ')[0];
return ${shortName}:${item.count};
});
label.innerHTML = ✨ ${total}人が作業中<br><span style="font-size:0.7rem;opacity:0.6;">${names.join(' · ')}</span>;
}
} catch (e) {
console.error('SolParS widget error:', e);
label.textContent = '';
}
}// --- アニメーションループ ---
function animate() {
ctx.clearRect(0, 0, 120, 120);
const all = [...dummies, ...particles];
all.forEach(p => { p.update(); p.draw(ctx); });
animId = requestAnimationFrame(animate);
}// --- 起動 ---
initDummies();
fetchAndUpdate();
setInterval(fetchAndUpdate, POLL_INTERVAL);
animate();// 20分ごとにダミー再生成(本家と同じ)
setInterval(initDummies, 20 * 60 * 1000);
})();
</script>
プライバシーポリシー収集する情報
当サービスでは、以下の情報を収集・保存します:
- セッション管理用の識別子
- 選択されたアクティビティの情報
- UI表示設定情報の利用目的
収集した情報は以下の目的でのみ使用します:
- サービスの基本機能の提供
- ユーザー体験の向上
- 匿名化された利用統計の作成データの保護
- 収集した情報は第三者への提供を行いません
- 統計情報として使用する場合も、個人を特定できない形で使用しますお問い合わせ
本ポリシーに関するお問い合わせは[連絡先]までご連絡ください<現在準備中です>最終更新日:2025年1月