حسگرهای وب، حسگرهای وب

از API حسگر عمومی برای دسترسی به حسگرهای روی دستگاه مانند شتاب‌سنج، ژیروسکوپ و مغناطیس‌سنج استفاده کنید.

الکس شالاموف
Alex Shalamov
میخائیل پوزدنیاکف
Mikhail Pozdnyakov

امروزه، داده‌های حسگر در بسیاری از برنامه‌های کاربردی مختص پلتفرم برای فعال کردن موارد استفاده مانند بازی‌های فراگیر، ردیابی تناسب اندام و واقعیت افزوده یا مجازی استفاده می‌شوند. آیا پر کردن شکاف بین برنامه‌های کاربردی مختص پلتفرم و وب جالب نخواهد بود؟ API حسگر عمومی را برای وب وارد کنید!

API حسگر عمومی چیست؟

API عمومی حسگر مجموعه‌ای از رابط‌ها است که دستگاه‌های حسگر را در معرض پلتفرم وب قرار می‌دهد. این API شامل رابط پایه Sensor و مجموعه‌ای از کلاس‌های حسگر عینی ساخته شده در بالا است. داشتن یک رابط پایه، فرآیند پیاده‌سازی و تعیین مشخصات کلاس‌های حسگر عینی را ساده می‌کند. برای مثال، به کلاس Gyroscope نگاهی بیندازید. این کلاس فوق‌العاده کوچک است! عملکرد اصلی توسط رابط پایه مشخص می‌شود و Gyroscope صرفاً آن را با سه ویژگی که نشان‌دهنده سرعت زاویه‌ای هستند، گسترش می‌دهد.

برخی از کلاس‌های حسگر با حسگرهای سخت‌افزاری واقعی مانند مثلاً کلاس‌های شتاب‌سنج یا ژیروسکوپ ارتباط برقرار می‌کنند. به این‌ها حسگرهای سطح پایین گفته می‌شود. حسگرهای دیگر، که به آن‌ها حسگرهای فیوژن گفته می‌شود، داده‌ها را از چندین حسگر سطح پایین ادغام می‌کنند تا اطلاعاتی را که یک اسکریپت در غیر این صورت باید محاسبه کند، نمایش دهند. به عنوان مثال، حسگر AbsoluteOrientation یک ماتریس چرخش چهار در چهار آماده برای استفاده بر اساس داده‌های به دست آمده از شتاب‌سنج، ژیروسکوپ و مغناطیس‌سنج ارائه می‌دهد.

ممکن است فکر کنید که پلتفرم وب از قبل داده‌های حسگر را ارائه می‌دهد و کاملاً درست فکر می‌کنید! برای مثال، رویدادهای DeviceMotion و DeviceOrientation داده‌های حسگر حرکت را نمایش می‌دهند. پس چرا به یک API جدید نیاز داریم؟

در مقایسه با رابط‌های موجود، API حسگر عمومی مزایای زیادی را ارائه می‌دهد:

  • API حسگر عمومی یک چارچوب حسگر است که می‌تواند به راحتی با کلاس‌های حسگر جدید گسترش یابد و هر یک از این کلاس‌ها رابط عمومی را حفظ می‌کنند. کد کلاینت نوشته شده برای یک نوع حسگر را می‌توان با تغییرات بسیار کمی برای نوع دیگری استفاده کرد!
  • شما می‌توانید سنسور را پیکربندی کنید. برای مثال، می‌توانید فرکانس نمونه‌برداری مناسب برای نیازهای کاربردی خود را تنظیم کنید.
  • می‌توانید تشخیص دهید که آیا حسگری روی پلتفرم موجود است یا خیر.
  • خوانش‌های حسگر دارای مهرهای زمانی با دقت بالا هستند که امکان همگام‌سازی بهتر با سایر فعالیت‌های برنامه شما را فراهم می‌کنند.
  • مدل‌های داده حسگر و سیستم‌های مختصات به وضوح تعریف شده‌اند و به فروشندگان مرورگر اجازه می‌دهند تا راه‌حل‌های سازگار را پیاده‌سازی کنند.
  • رابط‌های مبتنی بر حسگر عمومی به DOM محدود نیستند (به این معنی که آنها نه navigator هستند و نه اشیاء window )، و این فرصت‌های آینده را برای استفاده از API در سرویس ورکرها یا پیاده‌سازی آن در زمان‌های اجرای جاوا اسکریپت بدون سر، مانند دستگاه‌های تعبیه‌شده، باز می‌کند.
  • جنبه‌های امنیتی و حریم خصوصی اولویت اصلی API حسگر عمومی هستند و در مقایسه با APIهای حسگر قدیمی‌تر، امنیت بسیار بهتری را ارائه می‌دهند. با API مجوزها ادغام شده است.
  • همگام‌سازی خودکار با مختصات صفحه نمایش برای Accelerometer ، Gyroscope ، LinearAccelerationSensor ، حسگر جهت‌گیری AbsoluteOrientationSensor ، RelativeOrientationSensor و Magnetometer در دسترس است.

APIهای حسگر عمومی موجود

در زمان نگارش این مطلب، چندین حسگر وجود دارد که می‌توانید با آنها آزمایش کنید.

حسگرهای حرکتی:

  • Accelerometer
  • Gyroscope
  • LinearAccelerationSensor
  • AbsoluteOrientationSensor
  • RelativeOrientationSensor
  • GravitySensor

حسگرهای محیطی:

  • AmbientLightSensor (پشت پرچم #enable-generic-sensor-extra-classes در کرومیوم.)
  • Magnetometer (در کرومیوم، پشت علامت #enable-generic-sensor-extra-classes قرار دارد.)

تشخیص ویژگی

تشخیص ویژگی‌های APIهای سخت‌افزاری کار دشواری است، زیرا شما باید هم تشخیص دهید که آیا مرورگر از رابط کاربری مورد نظر پشتیبانی می‌کند یا خیر، و هم اینکه آیا دستگاه حسگر مربوطه را دارد یا خیر. بررسی اینکه آیا مرورگر از یک رابط کاربری پشتیبانی می‌کند یا خیر، ساده است. (به جای Accelerometer ، هر یک از رابط‌های کاربری دیگری که در بالا ذکر شد را قرار دهید.)

if ('Accelerometer' in window) {
  // The `Accelerometer` interface is supported by the browser.
  // Does the device have an accelerometer, though?
}

برای رسیدن به یک نتیجه‌ی تشخیص ویژگی واقعاً معنادار، باید اتصال به حسگر را نیز امتحان کنید. این مثال نحوه‌ی انجام این کار را نشان می‌دهد.

let accelerometer = null;
try {
  accelerometer = new Accelerometer({ frequency: 10 });
  accelerometer.onerror = (event) => {
    // Handle runtime errors.
    if (event.error.name === 'NotAllowedError') {
      console.log('Permission to access sensor was denied.');
    } else if (event.error.name === 'NotReadableError') {
      console.log('Cannot connect to the sensor.');
    }
  };
  accelerometer.onreading = (e) => {
    console.log(e);
  };
  accelerometer.start();
} catch (error) {
  // Handle construction errors.
  if (error.name === 'SecurityError') {
    console.log('Sensor construction was blocked by the Permissions Policy.');
  } else if (error.name === 'ReferenceError') {
    console.log('Sensor is not supported by the User Agent.');
  } else {
    throw error;
  }
}

پلی‌فیل

برای مرورگرهایی که از API حسگر عمومی پشتیبانی نمی‌کنند، یک polyfill در دسترس است. polyfill به شما امکان می‌دهد فقط پیاده‌سازی‌های حسگرهای مربوطه را بارگیری کنید.

// Import the objects you need.
import { Gyroscope, AbsoluteOrientationSensor } from './src/motion-sensors.js';

// And they're ready for use!
const gyroscope = new Gyroscope({ frequency: 15 });
const orientation = new AbsoluteOrientationSensor({ frequency: 60 });

این همه سنسور چی هستن؟ چطور می‌تونم ازشون استفاده کنم؟

حسگرها بخشی هستند که ممکن است نیاز به معرفی مختصری داشته باشند. اگر با حسگرها آشنا هستید، می‌توانید مستقیماً به بخش کدنویسی عملی بروید. در غیر این صورت، بیایید هر حسگر پشتیبانی شده را با جزئیات بررسی کنیم.

شتاب‌سنج و حسگر شتاب خطی

اندازه‌گیری‌های حسگر شتاب‌سنج

حسگر Accelerometer شتاب دستگاهی را که حسگر در آن قرار دارد، در سه محور (X، Y و Z) اندازه‌گیری می‌کند. این حسگر یک حسگر اینرسی است، به این معنی که وقتی دستگاه در حال سقوط آزاد خطی است، شتاب کل اندازه‌گیری شده 0 متر بر ثانیه مربع خواهد بود و وقتی دستگاه روی میز قرار دارد، شتاب در جهت بالا (محور Z) برابر با گرانش زمین خواهد بود، یعنی g ≈ +9.8 متر بر ثانیه مربع، زیرا نیروی میز که دستگاه را به سمت بالا هل می‌دهد، اندازه‌گیری می‌شود. اگر دستگاه را به سمت راست هل دهید، شتاب در محور X مثبت و اگر دستگاه از راست به چپ شتاب بگیرد، منفی خواهد بود.

شتاب‌سنج‌ها می‌توانند برای مواردی مانند شمارش گام، سنجش حرکت یا جهت‌یابی ساده دستگاه استفاده شوند. اغلب، اندازه‌گیری‌های شتاب‌سنج با داده‌های منابع دیگر ترکیب می‌شوند تا حسگرهای فیوژن، مانند حسگرهای جهت‌یابی، ایجاد شوند.

حسگر LinearAccelerationSensor شتابی را که به دستگاه میزبان حسگر اعمال می‌شود، بدون در نظر گرفتن سهم گرانش، اندازه‌گیری می‌کند. وقتی دستگاه در حالت سکون است، مثلاً روی میز دراز کشیده است، حسگر شتابی معادل ≈ 0 متر بر ثانیه مربع را در سه محور اندازه‌گیری می‌کند.

حسگر جاذبه

در حال حاضر کاربران می‌توانند با بررسی دستی داده‌های Accelerometer و LinearAccelerometer ، به صورت دستی به مقادیری نزدیک به مقادیر حسگر گرانش دست یابند، اما این کار می‌تواند دشوار باشد و به دقت مقادیر ارائه شده توسط این حسگرها بستگی دارد. پلتفرم‌هایی مانند اندروید می‌توانند داده‌های گرانش را به عنوان بخشی از سیستم عامل ارائه دهند که از نظر محاسبات ارزان‌تر، بسته به سخت‌افزار کاربر، مقادیر دقیق‌تری ارائه می‌دهد و از نظر ارگونومی API، استفاده از آن آسان‌تر است. GravitySensor اثر شتاب در امتداد محورهای X، Y و Z دستگاه ناشی از گرانش را برمی‌گرداند.

ژیروسکوپ

اندازه‌گیری‌های حسگر ژیروسکوپ

حسگر Gyroscope سرعت زاویه‌ای را بر حسب رادیان بر ثانیه حول محورهای X، Y و Z محلی دستگاه اندازه‌گیری می‌کند. اکثر دستگاه‌های مصرفی دارای ژیروسکوپ‌های مکانیکی ( MEMS ) هستند که حسگرهای اینرسی هستند و سرعت چرخش را بر اساس نیروی اینرسی کوریولیس اندازه‌گیری می‌کنند. ژیروسکوپ‌های MEMS مستعد رانش هستند که ناشی از حساسیت گرانشی حسگر است و سیستم مکانیکی داخلی حسگر را تغییر شکل می‌دهد. ژیروسکوپ‌ها در فرکانس‌های نسبتاً بالای، مثلاً 10 ثانیه کیلوهرتز، نوسان می‌کنند و بنابراین، ممکن است در مقایسه با سایر حسگرها، انرژی بیشتری مصرف کنند.

حسگرهای جهت‌یابی

اندازه‌گیری‌های حسگر جهت‌گیری مطلق

حسگر AbsoluteOrientationSensor یک حسگر فیوژن است که چرخش یک دستگاه را نسبت به سیستم مختصات زمین اندازه‌گیری می‌کند، در حالی که حسگر جهت‌یابی RelativeOrientationSensor داده‌هایی را ارائه می‌دهد که نشان‌دهنده چرخش دستگاهی است که میزبان حسگرهای حرکتی است و نسبت به یک سیستم مختصات مرجع ثابت عمل می‌کند.

تمام فریم‌ورک‌های مدرن سه‌بعدی جاوااسکریپت از کواترنیون‌ها و ماتریس‌های چرخش برای نمایش چرخش پشتیبانی می‌کنند؛ با این حال، اگر مستقیماً از WebGL استفاده کنید، OrientationSensor به راحتی هم ویژگی quaternion و هم متد populateMatrix() را دارد. در اینجا چند قطعه کد آورده شده است:

سه.js

let torusGeometry = new THREE.TorusGeometry(7, 1.6, 4, 3, 6.3);
let material = new THREE.MeshBasicMaterial({ color: 0x0071c5 });
let torus = new THREE.Mesh(torusGeometry, material);
scene.add(torus);

// Update mesh rotation using quaternion.
const sensorAbs = new AbsoluteOrientationSensor();
sensorAbs.onreading = () => torus.quaternion.fromArray(sensorAbs.quaternion);
sensorAbs.start();

// Update mesh rotation using rotation matrix.
const sensorRel = new RelativeOrientationSensor();
let rotationMatrix = new Float32Array(16);
sensor_rel.onreading = () => {
  sensorRel.populateMatrix(rotationMatrix);
  torus.matrix.fromArray(rotationMatrix);
};
sensorRel.start();

بابل

const mesh = new BABYLON.Mesh.CreateCylinder('mesh', 0.9, 0.3, 0.6, 9, 1, scene);
const sensorRel = new RelativeOrientationSensor({ frequency: 30 });
sensorRel.onreading = () => mesh.rotationQuaternion.FromArray(sensorRel.quaternion);
sensorRel.start();

وب‌جی‌ال

// Initialize sensor and update model matrix when new reading is available.
let modMatrix = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
const sensorAbs = new AbsoluteOrientationSensor({ frequency: 60 });
sensorAbs.onreading = () => sensorAbs.populateMatrix(modMatrix);
sensorAbs.start();

// Somewhere in rendering code, update vertex shader attribute for the model
gl.uniformMatrix4fv(modMatrixAttr, false, modMatrix);

حسگرهای جهت‌یابی موارد استفاده مختلفی مانند بازی‌های همه‌جانبه، واقعیت افزوده و واقعیت مجازی را امکان‌پذیر می‌کنند.

برای اطلاعات بیشتر در مورد سنسورهای حرکتی، موارد استفاده پیشرفته و الزامات، به سند توضیح سنسورهای حرکتی مراجعه کنید.

همگام‌سازی با مختصات صفحه نمایش

به طور پیش‌فرض، داده‌های حسگرهای مکانی در یک سیستم مختصات محلی که به دستگاه متصل است، تجزیه و تحلیل می‌شوند و جهت صفحه نمایش را در نظر نمی‌گیرند.

سیستم مختصات دستگاه
سیستم مختصات دستگاه

با این حال، بسیاری از موارد استفاده مانند بازی‌ها یا واقعیت افزوده و مجازی نیاز دارند که خوانش‌های حسگر در یک سیستم مختصات که به جهت صفحه نمایش محدود است، حل شوند.

سیستم مختصات صفحه نمایش
سیستم مختصات صفحه نمایش

پیش از این، نگاشت مجدد داده‌های حسگر به مختصات صفحه نمایش باید در جاوا اسکریپت پیاده‌سازی می‌شد. این رویکرد ناکارآمد است و همچنین پیچیدگی کد برنامه وب را به میزان قابل توجهی افزایش می‌دهد؛ برنامه وب باید تغییرات جهت صفحه نمایش را مشاهده کند و تبدیل مختصات را برای داده‌های حسگر انجام دهد، که برای زوایای اویلر یا کواترنیون‌ها کار ساده‌ای نیست.

API حسگر عمومی (Generic Sensor API) یک راه‌حل بسیار ساده‌تر و قابل اعتمادتر ارائه می‌دهد! سیستم مختصات محلی برای همه کلاس‌های حسگر مکانی تعریف‌شده قابل پیکربندی است: AccelerometerGyroscope ، حسگر LinearAccelerationSensor ، حسگر جهت‌گیری AbsoluteOrientationSensor ، RelativeOrientationSensor ) و Magnetometer ). با ارسال گزینه referenceFrame به سازنده شیء حسگر، کاربر تعریف می‌کند که آیا مقادیر خوانده‌شده در مختصات دستگاه یا صفحه نمایش نمایش داده شوند.

// Sensor readings are resolved in the Device coordinate system by default.
// Alternatively, could be RelativeOrientationSensor({referenceFrame: "device"}).
const sensorRelDevice = new RelativeOrientationSensor();

// Sensor readings are resolved in the Screen coordinate system. No manual remapping is required!
const sensorRelScreen = new RelativeOrientationSensor({ referenceFrame: 'screen' });

بیایید کد بزنیم!

رابط برنامه‌نویسی کاربردی عمومی حسگر (Generic Sensor API) بسیار ساده و آسان برای استفاده است! رابط حسگر دارای متدهای start() و stop() برای کنترل وضعیت حسگر و چندین رویداد برای دریافت اعلان‌ها در مورد فعال شدن حسگر، خطاها و داده‌های جدید است. کلاس‌های حسگر واقعی معمولاً ویژگی‌های خواندن خاص خود را به کلاس پایه اضافه می‌کنند.

محیط توسعه

در طول توسعه، شما قادر خواهید بود از طریق localhost از حسگرها استفاده کنید. اگر در حال توسعه برای دستگاه‌های تلفن همراه هستید، port forwarding را برای سرور محلی خود تنظیم کنید و آماده شروع به کار هستید!

وقتی کد شما آماده شد، آن را روی سروری که از HTTPS پشتیبانی می‌کند، مستقر کنید. صفحات گیت‌هاب از طریق HTTPS ارائه می‌شوند و این باعث می‌شود که این مکان، مکانی عالی برای به اشتراک گذاشتن دموهای شما باشد.

چرخش مدل سه‌بعدی

در این مثال ساده، ما از داده‌های یک حسگر جهت‌یابی مطلق برای تغییر چهارگانه‌ی چرخش یک مدل سه‌بعدی استفاده می‌کنیم. این model یک نمونه از کلاس Object3D در three.js است که دارای ویژگی quaternion است. قطعه کد زیر از نسخه آزمایشی تلفن جهت‌یابی ، نحوه‌ی استفاده از حسگر جهت‌یابی مطلق را برای چرخش یک مدل سه‌بعدی نشان می‌دهد.

function initSensor() {
  sensor = new AbsoluteOrientationSensor({ frequency: 60 });
  sensor.onreading = () => model.quaternion.fromArray(sensor.quaternion);
  sensor.onerror = (event) => {
    if (event.error.name == 'NotReadableError') {
      console.log('Sensor is not available.');
    }
  };
  sensor.start();
}

جهت‌گیری دستگاه در چرخش model سه‌بعدی در صحنه WebGL منعکس خواهد شد.

حسگر، جهت‌گیری مدل سه‌بعدی را به‌روزرسانی می‌کند
حسگر، جهت‌گیری یک مدل سه‌بعدی را به‌روزرسانی می‌کند

پانچ متر

قطعه کد زیر از نسخه آزمایشی پانچ‌متر استخراج شده است و نشان می‌دهد که چگونه می‌توان از حسگر شتاب خطی برای محاسبه حداکثر سرعت یک دستگاه با فرض اینکه در ابتدا ثابت است، استفاده کرد.

this.maxSpeed = 0;
this.vx = 0;
this.ax = 0;
this.t = 0;

/* … */

this.accel.onreading = () => {
  let dt = (this.accel.timestamp - this.t) * 0.001; // In seconds.
  this.vx += ((this.accel.x + this.ax) / 2) * dt;

  let speed = Math.abs(this.vx);

  if (this.maxSpeed < speed) {
    this.maxSpeed = speed;
  }

  this.t = this.accel.timestamp;
  this.ax = this.accel.x;
};

سرعت جریان به عنوان تقریبی از انتگرال تابع شتاب محاسبه می‌شود.

نسخه آزمایشی برنامه وب برای اندازه‌گیری سرعت پانچ
اندازه‌گیری سرعت ضربه

اشکال‌زدایی و لغو حسگر با Chrome DevTools

در برخی موارد، برای کار با Generic Sensor API نیازی به دستگاه فیزیکی ندارید. Chrome DevTools پشتیبانی بسیار خوبی برای شبیه‌سازی جهت‌گیری دستگاه دارد.

Chrome DevTools برای لغو داده‌های جهت‌گیری سفارشی یک تلفن مجازی استفاده می‌شود.
شبیه‌سازی جهت‌گیری دستگاه با Chrome DevTools

حریم خصوصی و امنیت

داده‌های حسگر، داده‌های حساسی هستند که می‌توانند در معرض حملات مختلف از صفحات وب مخرب قرار گیرند. پیاده‌سازی APIهای حسگر عمومی، محدودیت‌هایی را برای کاهش خطرات احتمالی امنیتی و حریم خصوصی اعمال می‌کند. این محدودیت‌ها باید توسط توسعه‌دهندگانی که قصد استفاده از API را دارند، در نظر گرفته شوند، بنابراین بیایید به طور خلاصه آنها را فهرست کنیم.

فقط HTTPS

از آنجا که Generic Sensor API یک ویژگی قدرتمند است، مرورگر فقط در زمینه‌های امن اجازه استفاده از آن را می‌دهد. در عمل، این بدان معناست که برای استفاده از Generic Sensor API باید از طریق HTTPS به صفحه خود دسترسی داشته باشید. در طول توسعه می‌توانید این کار را از طریق http://localhost انجام دهید، اما برای تولید نهایی باید HTTPS را روی سرور خود داشته باشید. برای بهترین شیوه‌ها و دستورالعمل‌ها، به مجموعه Safe and secure مراجعه کنید.

ادغام سیاست مجوزها

ادغام سیاست مجوزها در API حسگر عمومی، دسترسی به داده‌های حسگرها را برای یک فریم کنترل می‌کند.

به طور پیش‌فرض، اشیاء Sensor فقط می‌توانند درون یک فریم اصلی یا زیرفریم‌های با منشأ یکسان ایجاد شوند، بنابراین از خواندن غیرمجاز داده‌های حسگر توسط iframeهای با منشأ متقابل جلوگیری می‌شود. این رفتار پیش‌فرض را می‌توان با فعال یا غیرفعال کردن صریح ویژگی‌های کنترل‌شده توسط سیاست مربوطه تغییر داد.

قطعه کد زیر، اعطای دسترسی به داده‌های شتاب‌سنج به یک iframe با مبدا متقابل را نشان می‌دهد، به این معنی که اکنون اشیاء Accelerometer یا LinearAccelerationSensor می‌توانند در آنجا ایجاد شوند.

<iframe src="https://third-party.com" allow="accelerometer" />

تحویل قرائت‌های حسگر می‌تواند به حالت تعلیق درآید

خوانش‌های حسگر فقط از طریق یک صفحه وب قابل مشاهده قابل دسترسی هستند، یعنی زمانی که کاربر واقعاً با آن تعامل دارد. علاوه بر این، اگر تمرکز کاربر به یک زیرفریم با مبدأ متقابل تغییر کند، داده‌های حسگر به فریم والد ارائه نمی‌شوند. این امر مانع از استنباط ورودی کاربر توسط فریم والد می‌شود.

بعدش چی؟

مجموعه‌ای از کلاس‌های حسگر از پیش مشخص‌شده وجود دارد که قرار است در آینده نزدیک پیاده‌سازی شوند، مانند حسگر نور محیط یا حسگر مجاورت ؛ با این حال، به لطف توسعه‌پذیری بالای چارچوب حسگر عمومی، می‌توانیم ظهور کلاس‌های جدید بیشتری را که نشان‌دهنده انواع مختلف حسگر هستند، پیش‌بینی کنیم.

یکی دیگر از زمینه‌های مهم برای کارهای آینده، بهبود خود API حسگر عمومی است، مشخصات حسگر عمومی در حال حاضر یک پیشنهاد کاندیدا است، به این معنی که هنوز زمان برای ایجاد اصلاحات و افزودن قابلیت‌های جدید مورد نیاز توسعه‌دهندگان وجود دارد.

شما می‌توانید کمک کنید!

مشخصات حسگر به سطح بلوغ توصیه‌نامه رسیده است، از این رو، از بازخورد توسعه‌دهندگان وب و مرورگر بسیار قدردانی می‌شود. به ما اطلاع دهید که چه ویژگی‌هایی برای اضافه شدن عالی هستند یا اگر چیزی هست که می‌خواهید در API فعلی تغییر دهید.

لطفاً در صورت تمایل، مشکلات مربوط به مشخصات و همچنین اشکالات مربوط به پیاده‌سازی کروم را ثبت کنید.

منابع

تقدیرنامه‌ها

این مقاله توسط جو مدلی و کیس باسک بررسی شده است.