diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fd99cf0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +# Normalize all text files to LF in the repo and on checkout, on every OS. +# Prevents the Windows/WSL CRLF churn that shows every line as "changed". +* text=auto eol=lf + +# Explicitly treat known binary assets as binary (never touch their bytes). +*.webm binary +*.mp4 binary +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.webp binary +*.wasm binary +*.ico binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0423d3d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: [master, v2] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Unit tests (Node) + run: npm test + + - name: Type-check declarations + run: npm run test:types + + - name: Build UMD bundle + run: npm run build + + # Chromium + its system libraries, via the playwright-core installer. + - name: Install Chromium + run: npx playwright-core install --with-deps chromium + + - name: Browser tests (headless Chromium) + run: npm run test:browser diff --git a/.gitignore b/.gitignore index fd1e27d..2f4862b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,5 @@ -# Logs -logs +node_modules/ *.log -npm-debug.log* - -# Dependency directories -node_modules - -# Optional npm cache directory -.npm - -# Optional REPL history -.node_repl_history - -# Local file system .DS_Store - -# IDE -.idea +dist/ +build/ diff --git a/.npmignore b/.npmignore deleted file mode 100644 index f42b4f8..0000000 --- a/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -examples -assets -extension -utils \ No newline at end of file diff --git a/LICENSE b/LICENSE index e097bea..9955da0 100755 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2012 Jaume Sanchez Elias +Copyright (c) 2012-2026 Jaume Sanchez Elias Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e86aeb5..b5f1b54 100755 --- a/README.md +++ b/README.md @@ -1,189 +1,644 @@ -# CCapture.js - A library to capture canvas-based animations -CCapture.js is a library to help capturing animations created with HTML5 `canvas` at a fixed framerate. - -- [What is CCapture.js and why would I need it?](#what-is-ccapturejs-and-why-would-i-need-it) -- [Using the code](#using-the-code) -- [Limitations](#limitations) -- [Gallery](#gallery) -- [Credits](#credits) -- [License](#license) +# CCapture -An example is probably worth a lot of words: [CCapture.js with Game of Life 3D](http://www.clicktorelease.com/code/conway3d_ccapture/). +Capture canvas-based animations at a **fixed framerate**, with motion blur and +modern encoders — a ground-up rewrite of [CCapture.js](https://github.com/spite/ccapture.js) +(ES modules, WebCodecs, GPU motion blur, `async`/`await`). - +Your animation renders as fast (or as slowly) as it needs to; CCapture lies to +it about what time it is, so the exported video is perfectly smooth at exactly +the framerate you asked for — 30, 60, 240, whatever. Record a 4K frame that takes +two seconds to draw and still get a flawless 60fps file. -#### What is CCapture.js and why would I need it? #### +```js +import CCapture from "ccapture.js"; -Let's say that you finally have your amazing **canvas**-based animation running in your browser, be it 2D or 3D with the power of **WebGL**. You've been working hard to keep it fast and smooth. If you're using `requestAnimationFrame` you're aiming for a framerate of 60fps or, in other words, each frame is taking 16ms or less to render. +const capturer = new CCapture({ format: "mp4", framerate: 60, timeLimit: 5 }); +capturer.start(); -Now you want to record a video of it. Not a big deal, you can fire up a screen capture software that churns out a video file and be done with it. But what if you wanted to create an HD movie of your animation, and it simply cannot be rendered at higher resolutions because frames start dropping? What if you wanted to put all the quality settings up for the video? What if you wanted to push that particle count to 10 millions? +function render() { + requestAnimationFrame(render); // your normal loop + drawMyScene(); // draws using performance.now() / rAF time + capturer.capture(canvas); // ← the only line you add +} +render(); +``` -What if, indeed. What would happen is that you'd get a choppy video at best. At higher resolutions, fillrate is a bottleneck for most **canvas**-based animations. High quality settings or high number of elements may be only feasible on more powerful hardware. +That's it. After 5 seconds an `.mp4` downloads. No `await`, no restructuring. + +`CCapture` is the unified, plug-and-play recorder. It's composed of two pieces +you can also use on their own: **`FrameWrap`** (canvas capture — motion blur + +encoding) and **`TimeWarp`** (the virtual clock). + +--- + +## Table of contents + +- [How it works](#how-it-works) +- [Install](#install) +- [The least-intrusive API](#the-least-intrusive-api) +- [Adding it to an existing sketch](#adding-it-to-an-existing-sketch) +- [Options](#options) +- [Formats & encoders](#formats--encoders) +- [Custom encoders](#custom-encoders) +- [Motion blur](#motion-blur) +- [Methods & events](#methods--events) +- [Recipes](#recipes) +- [Using TimeWarp on its own](#using-timewarp-on-its-own) +- [Using FrameWrap on its own](#using-framewrap-on-its-own) +- [The `hookDate` trade-off](#the-hookdate-trade-off) +- [Migrating from CCapture 1.x](#migrating-from-ccapture-1x) +- [Browser support & caveats](#browser-support--caveats) +- [Examples](#examples) +- [License](#license) -With CCapture.js you can record smooth videos at a fixed framerate for all these situations, because it doesn't run in realtime: it makes the animations run at a given, fixed framerate which can be specified. You can record animations at smooth and consistent 30 or 60fps even if each frame takes seconds to render. You can even take a 240fps capture and create motion blur with post-production software. +--- -The only requirement is that you step your values per frame according to elapsed time. In other words, don't increment your variables with a fixed value each frame, but use an elapsed time delta to adjust those increments. CCapture.js works by hooking the common methods for obtaining that elapsed time: `Date.now()`, `setTimeout`, `requestAnimationFrame`, etc. and making them behave like a constant time step is happening, fixed by the specified framerate. +## How it works -Methods supported so far: +The idea (à la [ryg's kkapture](http://www.farb-rausch.de/~fg/kkapture/)) is to +replace the browser's notion of time with a **virtual clock** that only advances +one fixed step per captured frame. Anything that derives motion from elapsed +time then steps deterministically, decoupled from wall-clock. -- `Date.now`, `Date.prototype.getTime` -- `setTimeout`, `clearTimeout`, `setInterval` (`clearInterval` pending) -- `requestAnimationFrame` -- `performance.now` -- `HTMLVideoElement.prototype.currentTime`, `HTMLAudioElement.prototype.currentTime` +The library is three composable pieces: -CCapture.js is more or less [ryg's kkapture](http://www.farb-rausch.de/~fg/kkapture/) but for JavaScript and `canvas`. +- **`TimeWarp`** — owns time. Hooks `performance.now`, `requestAnimationFrame`, + `setTimeout`/`setInterval`, `Date`, and media `currentTime`, and advances a + virtual clock on demand. Knows nothing about canvases. +- **`FrameWrap`** — owns capture. Takes already-drawn canvas frames, applies + motion blur, enforces frame/time limits, and feeds a pluggable encoder. Knows + nothing about time. +- **`CCapture`** — composes the two and drives them: the plug-and-play recorder. -The library supports multiple export formats using modular encoders (`CCFrameEncoder): +Because `requestAnimationFrame` is hooked during capture, your render loop only +advances when `CCapture.capture()` flushes it — so it paces the whole thing, as +fast as your machine can draw and encode, with no wall-clock throttle. -- `CCWebMEncoder` uses [WebM Writer for JavaScript](https://github.com/thenickdude/webm-writer-js/) to create a WebM movie -- `CCPNGEncoder` and `CCJPEGEncoder` export PNG and JPEG files in a TAR file, respectively -- `CCGIFEncoder` uses [gifjs](http://jnordberg.github.io/gif.js/) to create animated GIFs -- `CCFFMpegServerEncoder` uses [ffmpegserver.js](https://github.com/greggman/ffmpegserver.js) to generate video on the server +Use `CCapture` for the normal "record a live animation" case; reach for the +halves directly when you only need one (timing without capture, or encoding +frames you produce yourself without touching the browser clock). -Forks, pull requests and code critiques are welcome! +--- -#### Using the code #### +## Install -Include CCapture[.min].js and [WebM Writer](https://github.com/thenickdude/webm-writer-js) or [gifjs](http://jnordberg.github.io/gif.js/). +```bash +npm install ccapture.js +``` + +```js +import CCapture from "ccapture.js"; +// or: import { CCapture, FrameWrap, TimeWarp } from "ccapture.js"; +``` + +Or use it straight from source as native ES modules (no build step) — point at +`src/index.js` and serve over HTTP. + +Ships with **TypeScript declarations** (`.d.ts`) — full autocomplete and +type-checking out of the box, no `@types` package needed. + +### Script tag (no build, no bundler) + +A UMD/IIFE global build is included for classic ` - - - - - - - - -``` -Or include the whole pack -```html - + + ``` -Or use npm or bower to install the [package](https://www.npmjs.com/package/ccapture.js): -```bash -npm install ccapture.js + +Build it yourself with `npm run build` (outputs `build/ccapture.umd[.min].js`). + +--- + +## The least-intrusive API + +The common case is a single `capture(canvas)` call inside your existing loop: + +```js +const capturer = new CCapture({ format: "mp4", framerate: 60, timeLimit: 5 }); +capturer.start(); + +function render() { + requestAnimationFrame(render); + // draw using performance.now() (warped) or the rAF timestamp + capturer.capture(canvas); +} +render(); ``` -Or use bower to install the [package](https://www.npmjs.com/package/ccapture.js): -```bash -bower install ccapture.js + +Notes: +- Set a `timeLimit` or `frameLimit` and it stops and saves itself. Otherwise call + `capturer.stop()` then `capturer.save()` when you're done. +- Kick your loop once (`render()`), or make sure it's already running, right after + `start()` — after that, `capture()` drives it. +- The frame you pass to `capture()` should already be drawn. `capture()` is + `async` but you don't need to `await` it. + +--- + +## Adding it to an existing sketch + +Three steps, whatever you're using: **(1)** make a `CCapture`, **(2)** `start()` +it, **(3)** call `capture(yourCanvas)` once per frame in your loop. Drive motion +from `performance.now()` / the rAF timestamp / `millis()` (all warped during +capture) rather than fixed per-frame increments. + +**three.js** — pass the renderer's canvas: +```js +function animate() { + requestAnimationFrame(animate); + renderer.render(scene, camera); + capturer.capture(renderer.domElement); +} ``` -To create a CCapture object, write: +**p5.js** — p5 owns the loop, so drive it with `redraw()` while capturing (see +the [p5 example](examples/p5/)): +```js +capturer.on("stop", () => loop()); // resume live preview when done +await capturer.start(); +noLoop(); +(function tick() { + if (!capturer.capturing) return; + requestAnimationFrame(tick); + redraw(); // your draw() on the warped clock + capturer.capture(document.querySelector("canvas")); +})(); +``` +Global-mode p5 users can drop in the UMD build (`window.CCapture`) with two +` +
+ +The canvas has a transparent background (checkerboard shows through). Record + as PNG (or WebP) to get an image sequence with alpha for compositing. + MP4 has no alpha; the PNG/WebP paths keep it.
+ + + + + + + - -Example of ccapture.js using Game of Life in 3D.
-This is a simple example of how to add a few lines of code to capture video out of a canvas-based animation.
-Framerate
-
-
-
-
Format
-
-
-
-
Other options
-
-
-
-
-
-
-
-
-
-
-
-
-
-diff --git a/examples/CaptureUI.js b/examples/CaptureUI.js new file mode 100644 index 0000000..0d65744 --- /dev/null +++ b/examples/CaptureUI.js @@ -0,0 +1,275 @@ +import { CCapture, GifskiEncoder, renderAudioFrames } from "../src/index.js"; + +// Opt-in high-quality GIF (gifski-wasm, loaded lazily from a CDN on first use). +CCapture.registerEncoder("gif-hq", GifskiEncoder); + +/** + * A shared capture control panel for the examples. + * + * Minimal use: pass a `canvas` and a `render(t, ctx)` that draws one frame at + * the (warped) time `t`. CaptureUI owns format / framerate / motion-blur / + * duration selection, an idle preview, the capture loop, progress + frame count. + * + * Extras for the richer examples: + * - `media` — video/audio element(s) to frame-step during capture + * - `audio` — an AudioBuffer to mux as an audio track + * - `analyze` — precompute per-frame audio analysis (from `audio`) and pass it + * to render as `ctx.analysis` ({ frequency, waveform }) + * - `accept` + `onFile(file)` — show a file input; onFile returns + * `{ media?, audio? }` and may resize the canvas + * + * `render(t, ctx)` — ctx = { frame, analysis }. Existing `render(t)` still works. + */ + +let stylesInjected = false; +function injectStyles() { + if (stylesInjected) return; + stylesInjected = true; + const css = ` + .ccui { position: fixed; top: 1rem; left: 1rem; z-index: 99999; + width: 250px; background: #16161aee; color: #eee; border: 1px solid #333; + border-radius: 10px; padding: 14px; backdrop-filter: blur(6px); + font: 13px/1.5 system-ui, sans-serif; box-shadow: 0 8px 30px #0008; } + .ccui h2 { margin: 0 0 10px; font-size: 14px; letter-spacing: .02em; } + .ccui label { display: block; margin: 8px 0 3px; color: #9aa; font-size: 11px; + text-transform: uppercase; letter-spacing: .05em; } + .ccui select, .ccui input[type=number], .ccui input[type=file] { width: 100%; + box-sizing: border-box; background: #0e0e12; color: #eee; border: 1px solid #333; + border-radius: 6px; padding: 6px 8px; font: inherit; } + .ccui .check { display: flex; align-items: center; gap: 8px; margin-top: 10px; + color: #ccc; text-transform: none; letter-spacing: 0; font-size: 13px; } + .ccui .check input { flex: 0; width: auto; } + .ccui button { width: 100%; margin-top: 12px; padding: 9px; border: 0; + border-radius: 7px; font: 600 13px system-ui; cursor: pointer; color: #fff; } + .ccui button.start { background: crimson; } + .ccui button.stop { background: #b8860b; } + .ccui button:disabled { background: #444; cursor: default; } + .ccui .bar { height: 6px; background: #0e0e12; border-radius: 4px; margin-top: 12px; overflow: hidden; } + .ccui .bar > i { display: block; height: 100%; width: 0%; background: crimson; transition: width .08s linear; } + .ccui .meta { display: flex; justify-content: space-between; margin-top: 8px; font-variant-numeric: tabular-nums; color: #bbb; } + .ccui .status { margin-top: 8px; min-height: 1.3em; color: #7fd1ff; font-size: 12px; } + .ccui .badge { display:inline-block; margin-top:6px; padding:2px 7px; border-radius:5px; background:#0e0e12; border:1px solid #333; color:#8a8; font-size:11px; } + `; + const style = document.createElement("style"); + style.textContent = css; + document.head.appendChild(style); +} + +function el(tag, attrs = {}, children = []) { + const node = document.createElement(tag); + Object.assign(node, attrs); + for (const c of [].concat(children)) { + node.append(c.nodeType ? c : document.createTextNode(c)); + } + return node; +} +const option = (value, label) => el("option", { value, textContent: label }); + +export function createCaptureUI(config) { + const { + canvas, + render, + title = "CCapture", + name = String(title).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "capture", + formats = ["mp4", "webm", "gif", "gif-hq", "png", "jpg", "webp"], + framerates = [24, 30, 60], + defaultFramerate = 60, + duration = 4, + motionBlur = true, + preview = true, + analyze = false, + accept = null, + onFile = null, + container = document.body, + } = config; + + injectStyles(); + + // Mutable capture sources (also settable on the returned object / via onFile). + let media = config.media || null; + let audio = config.audio || null; + let analysis = null; + + const formatSel = el("select", {}, formats.map((f) => option(f, f.toUpperCase()))); + const fpsSel = el("select", {}, framerates.map((f) => option(String(f), `${f} fps`))); + fpsSel.value = String(defaultFramerate); + const durInput = el("input", { type: "number", value: String(duration), min: "1", max: "60" }); + const mbCheck = el("input", { type: "checkbox" }); + const mbSamples = el("input", { type: "number", value: "8", min: "2", max: "64", disabled: true }); + mbCheck.addEventListener("change", () => (mbSamples.disabled = !mbCheck.checked)); + + const fileInput = accept ? el("input", { type: "file", accept }) : null; + + const button = el("button", { className: "start", textContent: "Start" }); + const barFill = el("i"); + const frameMeta = el("span", { textContent: "0 frames" }); + const timeMeta = el("span", { textContent: "0.0s" }); + const status = el("div", { className: "status", textContent: "Idle." }); + const backendBadge = el("span", { className: "badge", textContent: "" }); + + const children = [el("h2", { textContent: title })]; + if (fileInput) children.push(el("label", { textContent: "Source file" }), fileInput); + children.push( + el("label", { textContent: "Format" }), formatSel, + el("label", { textContent: "Framerate" }), fpsSel, + el("label", { textContent: "Duration (s)" }), durInput + ); + if (motionBlur) { + children.push( + el("div", { className: "check" }, [mbCheck, "Motion blur"]), + el("label", { textContent: "Samples" }), mbSamples + ); + } + children.push( + button, + el("div", { className: "bar" }, [barFill]), + el("div", { className: "meta" }, [frameMeta, timeMeta]), + status, + backendBadge + ); + const panel = el("div", { className: "ccui" }, children); + container.appendChild(panel); + + let idle = true; + let capturer = null; + + // Wait for a file before recording only if there's no default source yet. + if (fileInput) { + if (!media && !audio) button.disabled = true; + fileInput.addEventListener("change", async () => { + const file = fileInput.files[0]; + if (!file) return; + button.disabled = true; + status.textContent = "Loading…"; + try { + const res = (await onFile(file)) || {}; + if ("media" in res) media = res.media; + if ("audio" in res) audio = res.audio; + status.textContent = "Ready."; + button.disabled = false; + } catch (e) { + status.textContent = e.message; + } + }); + } + + function setControlsDisabled(disabled) { + for (const c of [formatSel, fpsSel, durInput, mbCheck, mbSamples, fileInput]) { + if (c) c.disabled = disabled || (c === mbSamples && !mbCheck.checked); + } + } + + function previewLoop() { + if (!idle) return; + const t = performance.now() / 1000; + render(t, { frame: 0, analysis: undefined }); + requestAnimationFrame(previewLoop); + } + if (preview) previewLoop(); + + function toIdle() { + idle = true; + button.textContent = "Start"; + button.className = "start"; + button.disabled = false; + setControlsDisabled(false); + backendBadge.textContent = ""; + if (preview) previewLoop(); + } + + async function start() { + if (!idle) return; + idle = false; + + const framerate = +fpsSel.value; + let dur = Math.max(1, +durInput.value || duration); + if (media && Number.isFinite(media.duration)) dur = Math.min(dur, media.duration); + const samples = mbCheck.checked ? Math.max(2, +mbSamples.value || 8) : 1; + const targetFrames = Math.round(dur * framerate); + + if (capturer) { + try { await capturer.dispose(); } catch {} + capturer = null; + } + + // Precompute frame-accurate audio analysis if requested. + analysis = null; + if (analyze && audio) { + status.textContent = "Analyzing audio…"; + try { + analysis = await renderAudioFrames({ buffer: audio, framerate, frames: targetFrames, fftSize: 1024, smoothing: 0.7 }); + } catch (e) { + status.textContent = `Audio analysis failed: ${e.message}`; + } + } + + try { + capturer = new CCapture({ + format: formatSel.value, + name, + framerate, + timeLimit: dur, + quality: 95, + motionBlurFrames: samples, + media, + audio, + }); + } catch (e) { + status.textContent = e.message; + toIdle(); + return; + } + + let badged = false; + capturer.on("frame", (n) => { + frameMeta.textContent = `${n} / ${targetFrames} frames`; + timeMeta.textContent = `${(n / framerate).toFixed(1)}s`; + barFill.style.width = `${Math.min(100, (n / targetFrames) * 100)}%`; + if (!badged && capturer.motionBlur) { + badged = true; + backendBadge.textContent = `${capturer.motionBlur.gl ? "GPU" : "CPU"} blur · ${samples}×`; + } + }); + capturer.on("progress", (p) => { + if (!capturer.capturing) status.textContent = `Encoding… ${Math.round(p * 100)}%`; + }); + capturer.on("save", () => (status.textContent = "Saved — check downloads.")); + capturer.on("error", (e) => (status.textContent = `Error: ${e.message}`)); + capturer.on("stop", () => toIdle()); + + setControlsDisabled(true); + button.textContent = "Stop"; + button.className = "stop"; + status.textContent = "Recording…"; + barFill.style.width = "0%"; + backendBadge.textContent = samples > 1 ? "blur pending…" : ""; + + await capturer.start(); + + let t0 = null; + function loop() { + if (!capturer.capturing) return; + requestAnimationFrame(loop); + if (t0 === null) t0 = performance.now(); + const frame = capturer.frameCount; + render((performance.now() - t0) / 1000, { frame, analysis: analysis ? analysis[frame] : undefined }); + capturer.capture(canvas); + } + loop(); + } + + async function stop() { + if (!capturer || !capturer.capturing) return; + button.disabled = true; + status.textContent = "Finalizing…"; + await capturer.stop(); + await capturer.save(); + } + + button.addEventListener("click", () => (idle ? start() : stop())); + + const ui = { panel, get capturer() { return capturer; } }; + Object.defineProperty(ui, "media", { get: () => media, set: (v) => (media = v) }); + Object.defineProperty(ui, "audio", { get: () => audio, set: (v) => (audio = v) }); + return ui; +} diff --git a/examples/alpha/index.html b/examples/alpha/index.html new file mode 100644 index 0000000..b7ae90d --- /dev/null +++ b/examples/alpha/index.html @@ -0,0 +1,49 @@ +
+ +
+ + +
diff --git a/examples/audio/index.html b/examples/audio/index.html new file mode 100644 index 0000000..4e9d4b5 --- /dev/null +++ b/examples/audio/index.html @@ -0,0 +1,75 @@ +
+ +
+ + +
diff --git a/examples/basic/buttons.css b/examples/basic/buttons.css deleted file mode 100755 index dfc04a4..0000000 --- a/examples/basic/buttons.css +++ /dev/null @@ -1,79 +0,0 @@ -.button { display: inline-block; padding: 7px 9px; font-size: inherit; color: #3C3C3D; text-shadow: 1px 1px 0 #FFFFFF; background: #ECECEC; white-space: nowrap; overflow: visible; cursor: pointer; text-decoration: none; border: 1px solid #CACACA; -webkit-border-radius: 2px; -moz-border-radius: 2px; -webkit-background-clip: padding-box; border-radius: 2px; outline: none; position: relative; zoom: 1; *display: inline; } -.button.primary { font-weight: bold } -.button:hover { color: #FFFFFF; border-color: #388AD4; text-decoration: none; text-shadow: -1px -1px 0 rgba(0,0,0,0.3); background-position: 0 -40px; background-color: #2D7DC5; } -.button:active, -.button.active { background-position: 0 -81px; border-color: #347BBA; background-color: #0F5EA2; color: #FFFFFF; text-shadow: none; } -.button:active { top: 1px } -.button.negative:hover { color: #FFFFFF; background-position: 0 -121px; background-color: #D84743; border-color: #911D1B; } -.button.negative:active, -.button.negative.active { background-position: 0 -161px; background-color: #A5211E; border-color: #911D1B; } -.button.pill { -webkit-border-radius: 19px; -moz-border-radius: 19px; border-radius: 19px; padding: 6px 12px; } -.button.left { -webkit-border-bottom-right-radius: 0px; -webkit-border-top-right-radius: 0px; -moz-border-radius-bottomright: 0px; -moz-border-radius-topright: 0px; border-bottom-right-radius: 0px; border-top-right-radius: 0px; margin-right: 0px; } -.button.middle { margin-right: 0px; margin-left: 0px; -webkit-border-radius: 0px; -moz-border-radius: 0px; border-radius: 0px; border-left: none; } -.button.right { -webkit-border-bottom-left-radius: 0px; -webkit-border-top-left-radius: 0px; -moz-border-radius-bottomleft: 0px; -moz-border-radius-topleft: 0px; border-top-left-radius: 0px; border-bottom-left-radius: 0px; margin-left: 0px; border-left: none;} -.button.left:active, -.button.middle:active, -.button.right:active { top: 0px } -.button.big { font-size: 16px; padding: 7px 16px; } -.button span.icon { display: inline-block; width: 14px; height: 12px; margin: auto 7px auto auto; position: relative; top: 1px; } -.big.button span.icon { top: 0px } -.button span.icon.book { background-position: 0 0 } -.button:hover span.icon.book { background-position: 0 -15px } -.button span.icon.calendar { background-position: 0 -30px } -.button:hover span.icon.calendar { background-position: 0 -45px } -.button span.icon.chat { background-position: 0 -60px } -.button:hover span.icon.chat { background-position: 0 -75px } -.button span.icon.check { background-position: 0 -90px } -.button:hover span.icon.check { background-position: 0 -103px } -.button span.icon.clock { background-position: 0 -116px } -.button:hover span.icon.clock { background-position: 0 -131px } -.button span.icon.cog { background-position: 0 -146px } -.button:hover span.icon.cog { background-position: 0 -161px } -.button span.icon.comment { background-position: 0 -176px } -.button:hover span.icon.comment { background-position: 0 -190px } -.button span.icon.cross { background-position: 0 -204px } -.button:hover span.icon.cross { background-position: 0 -219px } -.button span.icon.downarrow { background-position: 0 -234px } -.button:hover span.icon.downarrow { background-position: 0 -249px } -.button span.icon.fork { background-position: 0 -264px } -.button:hover span.icon.fork { background-position: 0 -279px } -.button span.icon.heart { background-position: 0 -294px } -.button:hover span.icon.heart { background-position: 0 -308px } -.button span.icon.home { background-position: 0 -322px } -.button:hover span.icon.home { background-position: 0 -337px } -.button span.icon.key { background-position: 0 -352px } -.button:hover span.icon.key { background-position: 0 -367px } -.button span.icon.leftarrow { background-position: 0 -382px } -.button:hover span.icon.leftarrow { background-position: 0 -397px } -.button span.icon.lock { background-position: 0 -412px } -.button:hover span.icon.lock { background-position: 0 -427px } -.button span.icon.loop { background-position: 0 -442px } -.button:hover span.icon.loop { background-position: 0 -457px } -.button span.icon.magnifier { background-position: 0 -472px } -.button:hover span.icon.magnifier { background-position: 0 -487px } -.button span.icon.mail { background-position: 0 -502px } -.button:hover span.icon.mail { background-position: 0 -514px } -.button span.icon.move { background-position: 0 -526px } -.button:hover span.icon.move { background-position: 0 -541px } -.button span.icon.pen { background-position: 0 -556px } -.button:hover span.icon.pen { background-position: 0 -571px } -.button span.icon.pin { background-position: 0 -586px } -.button:hover span.icon.pin { background-position: 0 -601px } -.button span.icon.plus { background-position: 0 -616px } -.button:hover span.icon.plus { background-position: 0 -631px } -.button span.icon.reload { background-position: 0 -646px } -.button:hover span.icon.reload { background-position: 0 -660px } -.button span.icon.rightarrow { background-position: 0 -674px } -.button:hover span.icon.rightarrow { background-position: 0 -689px } -.button span.icon.rss { background-position: 0 -704px } -.button:hover span.icon.rss { background-position: 0 -719px } -.button span.icon.tag { background-position: 0 -734px } -.button:hover span.icon.tag { background-position: 0 -749px } -.button span.icon.trash { background-position: 0 -764px } -.button:hover span.icon.trash { background-position: 0 -779px } -.button span.icon.unlock { background-position: 0 -794px } -.button:hover span.icon.unlock { background-position: 0 -809px } -.button span.icon.uparrow { background-position: 0 -824px } -.button:hover span.icon.uparrow { background-position: 0 -839px } -.button span.icon.user { background-position: 0 -854px } -.button:hover span.icon.user { background-position: 0 -869px } diff --git a/examples/basic/index.html b/examples/basic/index.html index 65e8723..51d79a6 100755 --- a/examples/basic/index.html +++ b/examples/basic/index.html @@ -1,417 +1,41 @@ - - -
-
- - - - - -
- -